I need some help. I figured out how to make a Circular Timer, but instead of that, I want to make it with Dates, and my example is using Int values. For example, I'm trying to countdown from a Future Date ( Example : February 4 11:00 PM ) to Date ( February 4 9:00 PM ).
This is the code that I wrote :
import SwiftUI
let timer = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
struct CircularTimer: View {
#State var counter: Int = 0
var countTo: Int = 120
var nowDate = Date()
var futureDate = Date.distantFuture
var body: some View {
VStack{
ZStack{
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().stroke(Color.green, lineWidth: 25)
)
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().trim(from:0, to: progress())
.stroke(
style: StrokeStyle(
lineWidth: 25,
lineCap: .round,
lineJoin:.round
)
)
.foregroundColor(
(completed() ? Color.orange : Color.red)
).animation(
.easeInOut(duration: 0.2)
)
)
}
}.onReceive(timer) { time in
if (self.counter < self.countTo) {
self.counter += 1
}
}
}
func completed() -> Bool {
return progress() == 1
}
func progress() -> CGFloat {
return (CGFloat(counter) / CGFloat(countTo))
}
}
I am also a novice in SwiftUI programming, but I tried to rewrite your solution to get a desired output. All you need is to work with
DateComponents and then with TimeInterval as those two allow you to represent Date() variables in seconds, so you can easily use it for your timer. Here it is:
import SwiftUI
let timer = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
struct CircularTimer: View {
// MARK: Changed type to TIMEINTERVAL
#State var counter: TimeInterval = 0
// MARK: New DATE variables using DATECOMPONENTS
#State var startDate: Date = Calendar.current.date(from: DateComponents(year: 2022, month: 2, day: 4, hour: 11, minute: 00, second: 10)) ?? Date.now
var endDate: Date = Calendar.current.date(from: DateComponents(year: 2022, month: 2, day: 4, hour: 11, minute: 00, second: 00)) ?? Date.now
// MARK: Calculated TIMEINTERVAL instead of original COUNTTO variable
var timeInterval: TimeInterval {
let end = endDate
let start = startDate
return start.timeIntervalSince(end)
}
var body: some View {
NavigationView {
VStack{
ZStack{
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().stroke(Color.green, lineWidth: 25)
)
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().trim(from:0, to: progress())
.stroke(
style: StrokeStyle(
lineWidth: 25,
lineCap: .round,
lineJoin:.round
)
)
.foregroundColor(
(completed() ? Color.orange : Color.red)
).animation(
.easeInOut(duration: 0.2)
)
)
// MARK: Text view for DATE just to show you that timer is working
Text("\(startDate.formatted(date: .long, time: .standard))")
.font(.system(size: 10))
.foregroundColor((timeInterval == 0) ? .orange : .black)
}
}
// MARK: Changed logic for timer calculations
.onReceive(timer) { time in
if (timeInterval != 0) {
counter += 1
startDate -= 1
}
}
// MARK: A few changes to the layout
.navigationTitle("Timer")
.toolbar {
Button("Start again", action: startAgain)
}
}
}
// MARK: Function for a START AGAIN button
func startAgain() {
counter = 0
startDate = Calendar.current.date(from: DateComponents(year: 2022, month: 2, day: 4, hour: 11, minute: 00, second: 10)) ?? Date.now
return
}
func completed() -> Bool {
return progress() == 1
}
func progress() -> CGFloat {
return (CGFloat(counter) / CGFloat(timeInterval + counter))
}
}
Related
I am looking to make a Streak builder app using Swift and SwiftUI. However, I am finding it difficult to create the logic for the counter using the Date(). Any suggestions will be highly appreciated.
Mainly I wanna replicate the Streak thing from the Duolingo app.
// I have this extension to follow up on the Date from the time user clicks it.
extension Date {
// for tomorow's Date
static var tomorrow: Date { return Date().dayAfter }
static var today: Date {return Date()}
var dayAfter: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: Date())!
}
}
// More or less this was supposed to be my view.
struct StreakTrial: View {
#State var counter = 0
#State var TapDate: Date = Date.today
var body: some View {
NavigationView {
VStack{
Button {
// if TapDate != Date.today {
// counter += 1
// let TapDate = Date.tomorrow
// }
// else if TapDate == Date.tomorrow {
// counter = counter
// }
} label: {
Image(systemName: "flame")
.resizable()
.frame(width: 40, height: 50)
.padding()
.scaledToFit()
.background(Color.gray)
.foregroundColor(Color.orange)
.cornerRadius(12)
Text("\(counter)").foregroundColor(.gray)
}
Text("\(Date())")
.padding()
Text("\(Date().dayAfter)")
.padding()
}
}
}
}
**SO I TRIED SOME TUTORIALS OF #NICK SARNO WHICH GOES LIKE SWIFTFUL THINKING ON YOUTUBE AND GOT IT DONE**
*This code is compatible with Xcode 14.0*
import SwiftUI
extension Date {
// for tomorow's Date
static var tomorrow: Date { return Date().dayAfter }
static var today: Date {return Date()}
var dayAfter: Date {
return Calendar.current.date(byAdding: .day, value: 1, to: Date())!
// just add .minute after byAdding: , to create a streak minute counter and check the logic.
}
static func getTodayDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E d MMM yyyy"
//to continue with the minute streak builder just add "E d MMM yyyy h:mm a" above, it will allow date formatting with minutes and follow the changes in dayAfter
return dateFormatter.string(from: Date.today)
}
static func getTomDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E d MMM yyyy"
return dateFormatter.string(from: Date.tomorrow)
}
}
struct StreakApp: View {
#AppStorage("counter") var counter = 0
#AppStorage("tapDate") var TapDate: String?
#AppStorage("Tappable") var ButtonTapped = false
var body: some View {
NavigationView {
VStack{
VStack {
Text("\(counter)").foregroundColor(.gray)
Text("Restore your streak on ")
Text(TapDate ?? "No Date")
Image(systemName: "flame")
.resizable()
.frame(width: 40, height: 50)
.padding()
.scaledToFit()
.background(ButtonTapped ? Color.red : Color.gray)
.foregroundColor(ButtonTapped ? Color.orange : Color.black)
.cornerRadius(12)
}
Button {
if TapDate == nil {
//Check if user has already tapped
self.ButtonTapped = true
counter += 1
self.TapDate = ("\(Date.getTomDate())")
}
else if ("\(Date.getTodayDate())") == TapDate {
//Check for the consecutive Day of Streak
self.TapDate = ("\(Date.getTomDate())")
counter += 1
//Let's light the flame back again.
self.ButtonTapped = true
}
} label: {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.foregroundColor(.black)
.frame(width: 120, height: 40)
.overlay {
Text("Add Streak")
.foregroundColor(.white)
}
}
.padding()
//This button is only for testing purpose.
Button {
self.TapDate = nil
self.ButtonTapped = false
self.counter = 0
} label: {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.foregroundColor(.black)
.frame(width: 160, height: 40)
.overlay {
Text("Reset Streak")
.foregroundColor(.white)
}
}
}
//Ensuer the flame dies out if we run into any other day except today or tommorow.
.onAppear {
if ("\(Date.getTodayDate())") == TapDate ||
("\(Date.getTomDate())") == TapDate {
self.ButtonTapped = true
}
//Breaking the Streak
else {
self.TapDate = nil
self.ButtonTapped = false
self.counter = 0
}
}
}
}
}
struct StreakApp_Previews: PreviewProvider {
static var previews: some View {
StreakApp()
}
}
I have an issue and I hope that I will find the answer here to my questions. I made a Circular Timer, but everytime I'm closing the view , is going back from the start.
I tried to save the value where the time is calculated "onTick", is working for 2-3 seconds, and after again is going back from the start and is countdown from there.
I'll put a picture/video bellow with the behaviour, maybe somone can help me fix it .
Thanks !
This is WaitingOrderView.
struct WaitingOrderView: View {
#State private var timer: AnyCancellable?
#EnvironmentObject var syncViewModel : SyncViewModel
var body: some View {
ZStack {
if syncViewModel._order.id == 0 && syncViewModel._order.status == 0 {
CartView()
}
else if syncViewModel._order.status == syncViewModel.statusList.first(where: { status in
status.key == StatusKey.accepted.rawValue
})?.id
{
OrderConfirmedView()
}
}
.onAppear() {
startFetchStatus()
}
}
func startFetchStatus() {
timer = Timer.publish(every: 20, on: .main, in: .common)
.autoconnect()
.sink { _ in
syncViewModel.fetchOrder(id: syncViewModel._order.id)
}
}
}
The function startFetchStatus() gets the data from the backend every 20 seconds, and it looks like this, for example: Fetch response
This is the CircularTimer View :
let timer = Timer
.publish(every: 5, on: .main, in: .common)
.autoconnect()
#available(iOS 15, *)
struct CircularTimer: View {
#EnvironmentObject var syncViewModel : SyncViewModel
#State var orderDate : Date
#State var orderDeliveryDate : Date
#State var onTick: Double = 1
#State var savedOnTick : Double = 1
let date = Date()
var body: some View {
VStack(spacing : 0){
Image("clock_button")
ZStack{
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().stroke(Color.gray.opacity(22/100), lineWidth: 5)
)
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().trim(from:0, to: onTick)
.stroke(
style: StrokeStyle(
lineWidth: 5,
lineCap: .round ,
lineJoin:.round
)
)
.foregroundColor(
( completed() ? Color.orange : Color.orange)
).animation(
.easeInOut(duration: 0.2)
)
)
.rotationEffect(Angle(degrees: 270.0))
Image("indicator_ellipse")
.resizable()
.frame(width: 230, height: 230)
}
.onAppear {
getData()
print(savedOnTick)
}
.onDisappear {
saveData()
}
}
.onReceive(timer) { time in
progress(time: Int(time.timeIntervalSince1970))
}
}
func completed() -> Bool {
return onTick == 1
}
func progress(time: Int) {
let minutesOrderDeliveryDate = Int(orderDeliveryDate.timeIntervalSince1970)
let minutesOrderDate = Int(orderDate.timeIntervalSince1970)
let minutesCurrentDate = time
let totalMinutes = minutesOrderDeliveryDate - minutesOrderDate
let remainingMinutes = minutesOrderDeliveryDate - minutesCurrentDate
onTick = CGFloat(remainingMinutes) / CGFloat(totalMinutes)
}
func dateFormatTime(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
return dateFormatter.date(from: date) ?? Date()
}
func saveData() {
UserDefaults.standard.set(onTick, forKey: "onTick")
}
func getData() {
onTick = UserDefaults.standard.double(forKey: "onTick")
}
}
This is OrderConfirmedView :
struct OrderConfirmedVieww: View {
#EnvironmentObject var syncViewModel : SyncViewModel
#State var nowDate: Date = Date()
var body: some View {
VStack {
Spacer()
Text(Texts.orderConfirmedText1)
.font(.title)
.fontWeight(.semibold)
.foregroundColor(.colorGrayDark)
.multilineTextAlignment(.center)
.lineLimit(3)
.padding(40)
CircularTimer(orderDate: dateFormatTime(date: syncViewModel._order.date ?? ""), orderDeliveryDate: dateFormatTime(date: syncViewModel._order.deliveryDate ?? ""))
.padding()
// Spacer()
Text(Texts.orderOraLivrareText)
.font(.headline)
.fontWeight(.thin)
.padding(.bottom)
Text(dateFormatTime(date: syncViewModel._order.deliveryDate ?? ""), style: .time)
.font(.title2)
.fontWeight(.bold)
Spacer()
Spacer()
Button {
} label: {
Text(Texts.orderButtonText)
.font(.headline)
.foregroundColor(.white)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(Color.onboardingColor)
.cornerRadius(20)
}
.padding()
}
}
func dateFormatTime(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.timeZone = .current
return dateFormatter.date(from: date) ?? Date.now
}
}
I've made a similiar question yesterday, but I have fixed the problem. Now I have another issue and I made a new question to ask.
I've made a Circular Timer which countdown : CircularTimer
But if I'm closing the view, and open it again, it starts again from scratch. How can I save the progress of the timer or something like that, so if I'm closing the view, when I'm coming back to be where it was last time ?
Thanks !
This is WaitingOrderView.
struct WaitingOrderView: View {
#State private var timer: AnyCancellable?
#EnvironmentObject var syncViewModel : SyncViewModel
var body: some View {
ZStack {
if syncViewModel._order.id == 0 && syncViewModel._order.status == 0 {
CartView()
}
else if syncViewModel._order.status == syncViewModel.statusList.first(where: { status in
status.key == StatusKey.accepted.rawValue
})?.id
{
OrderConfirmedView()
}
}
.onAppear() {
startFetchStatus()
}
}
func startFetchStatus() {
timer = Timer.publish(every: 20, on: .main, in: .common)
.autoconnect()
.sink { _ in
syncViewModel.fetchOrder(id: syncViewModel._order.id)
}
}
}
The function startFetchStatus() gets the data from the backend every 20 seconds, and it looks like this, for example: Fetch response
This is the CircularTimer View :
let timer = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
#available(iOS 15, *)
struct CircularTimer: View {
var orderDate : Date
var orderDeliveryDate : Date
#State var onTick: CGFloat = 1
let date = Date()
var body: some View {
VStack(spacing : 0){
Image("clock_button")
ZStack{
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().stroke(Color.gray.opacity(22/100), lineWidth: 5)
)
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().trim(from:0, to: onTick)
.stroke(
style: StrokeStyle(
lineWidth: 5,
lineCap: .round ,
lineJoin:.round
)
)
.foregroundColor(
( completed() ? Color.orange: Color.orange)
).animation(
.easeInOut(duration: 0.2)
)
)
.rotationEffect(Angle(degrees: 270.0))
Image("indicator_ellipse")
.resizable()
.frame(width: 230, height: 230)
}
}.onReceive(timer) { time in
progress(time: Int(time.timeIntervalSince1970))
}
}
func completed() -> Bool {
return onTick == 1
}
func progress(time: Int) {
let minutesOrderDeliveryDate = Int(orderDeliveryDate.timeIntervalSince1970)
let minutesOrderDate = Int(orderDate.timeIntervalSince1970)
let minutesCurrentDate = time
let totalMinutes = minutesOrderDeliveryDate - minutesOrderDate
let remainingMinutes = minutesOrderDeliveryDate - minutesCurrentDate
onTick = CGFloat(remainingMinutes) / CGFloat(totalMinutes)
print(onTick)
}
func dateFormatTime(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.timeZone = .current
return dateFormatter.date(from: date) ?? Date.now
}
}
This is OrderConfirmedView :
struct OrderConfirmedVieww: View {
#EnvironmentObject var syncViewModel : SyncViewModel
#State var nowDate: Date = Date()
var body: some View {
VStack {
Spacer()
Text(Texts.orderConfirmedText1)
.font(.title)
.fontWeight(.semibold)
.foregroundColor(.colorGrayDark)
.multilineTextAlignment(.center)
.lineLimit(3)
.padding(40)
CircularTimer(orderDate: dateFormatTime(date: syncViewModel._order.date ?? ""), orderDeliveryDate: dateFormatTime(date: syncViewModel._order.deliveryDate ?? ""))
.padding()
// Spacer()
Text(Texts.orderOraLivrareText)
.font(.headline)
.fontWeight(.thin)
.padding(.bottom)
Text(dateFormatTime(date: syncViewModel._order.deliveryDate ?? ""), style: .time)
.font(.title2)
.fontWeight(.bold)
Spacer()
Spacer()
Button {
} label: {
Text(Texts.orderButtonText)
.font(.headline)
.foregroundColor(.white)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(Color.onboardingColor)
.cornerRadius(20)
}
.padding()
}
}
func dateFormatTime(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.timeZone = .current
return dateFormatter.date(from: date) ?? Date.now
}
}
I have a problem and I dont know what's the issue, maybe you can help me out with this. I made a Circular Timer (a circle which count how much time is remaining), but I have 2 big issues .
After I close the view and come back, the timer is starting again from the start and it goes faster. And that's on repeat, after the view is again closed, it starts from the start and it goes faster.
Second issue is that I don't think is counting Right. For example "deliveryDate" is set to be at 4:00 PM, but is 3:58 PM and the Circular Timer is already completed.
NOTE : Is a delivery app, user can select the "deliveryDate".
My timer count with the help of 2 variables:
"syncViewModel._order.date" - This is the Date when the order is placed
and
"syncViewModel._order.deliveryDate". - This is the date that the user want the order to be delieverd
I'll share my code below
This is the Circular Timer :
let timer = Timer
.publish(every: 10, on: .main, in: .common)
.autoconnect()
#available(iOS 15, *)
struct CircularTimerr: View {
#EnvironmentObject var syncViewModel : SyncViewModel
#State var startPointValue : CGFloat
var endPointValue : CGFloat
var body: some View {
VStack{
ZStack{
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().stroke(Color.gray.opacity(22/100), lineWidth: 5)
)
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle().trim(from:0, to: progress())
.stroke(
style: StrokeStyle(
lineWidth: 5,
lineCap: .round,
lineJoin:.round
)
)
.foregroundColor(
(completed() ? Color.orange : Color.orange)
).animation(
.easeInOut(duration: 0.2)
)
) }
.onAppear{
print(startPointValue)
print(endPointValue)
}
}.onReceive(timer) { time in
if (self.startPointValue < self.endPointValue) {
self.startPointValue += 1
}
}
}
func completed() -> Bool {
return progress() == 1
}
func progress() -> CGFloat {
return ( startPointValue / endPointValue)
}
}
This is the view
struct OrderConfirmedVieww: View {
#EnvironmentObject var syncViewModel : SyncViewModel
var body: some View {
VStack {
CircularTimer(startPointValue: CGFloat(dateFormatTime(date: syncViewModel._order.date ?? "").timeIntervalSinceNow), endPointValue: CGFloat(dateFormatTime(date: syncViewModel._order.deliveryDate ?? "").timeIntervalSinceNow))
}
}
func dateFormatTime(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.timeZone = .current
return dateFormatter.date(from: date) ?? Date.now
}
}
Circular Timer Picture
I would like to develop an app that includes a timer - everything works fine so far - yet I have the problem that the CircleProgress is not quite at 0 when the counter is. (as you can see in the picture below)
Long story short - my timer is not precise... How can I make it better?
So this is my code:
This is the View where I give my Binding to the ProgressCircleView:
struct TimerView: View {
//every Second change Circle and Value (Circle is small because of the animation)
let timerForText = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timerForCircle = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
#State var progress : Double = 1.0
#State var counterCircle : Double = 0.0
#State var counterText : Int = 0
#State var timerSeconds : Int = 60
let customInverval : Int
var body: some View {
ZStack{
ProgressCircleView(progress: self.$progress, timerSeconds: self.$timerSeconds, customInterval: customInverval)
.padding()
.onReceive(timerForCircle){ time in
//if counterCircle is the same value as Interval -> break
if self.counterCircle == Double(customInverval){
self.timerForCircle.upstream.connect().cancel()
} else {
decreaseProgress()
}
counterCircle += 0.001
}
VStack{
Text("\(timerSeconds)")
.font(.system(size: 80))
.bold()
.onReceive(timerForText){time in
//wenn counterText is the same value as Interval -> break
if self.counterText == customInverval{
self.timerForText.upstream.connect().cancel()
} else {
incrementTimer()
print("timerSeconds: \(self.timerSeconds)")
}
counterText += 1
}.multilineTextAlignment(.center)
}
.accessibilityElement(children: .combine)
}.padding()
}
func decreaseProgress() -> Void {
let decreaseValue : Double = 1/(Double(customInverval)*1000)
self.progress -= decreaseValue
}
func incrementTimer() -> Void {
let decreaseValue = 1
self.timerSeconds -= decreaseValue
}
}
And this is my CircleProgressClass:
struct ProgressCircleView: View {
#Binding var progress : Double
#Binding var timerSeconds : Int
let customInterval : Int
var body: some View {
ZStack{
Circle()
.stroke(lineWidth: 25)
.opacity(0.08)
.foregroundColor(.black)
Circle()
.trim(from: 0.0, to: CGFloat(Double(min(progress, 1.0))))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(270.0))
.foregroundColor(getCircleColor(timerSeconds: timerSeconds))
.animation(.linear)
}
}
}
func getCircleColor(timerSeconds: Int) -> Color {
if (timerSeconds <= 10 && timerSeconds > 3) {
return Color.yellow
} else if (timerSeconds <= 3){
return Color.red
} else {
return Color.green
}
}
You cannot control the timer, it will never be entirely accurate.
Instead, I suggest you save the end date and calculate your progress based on it:
struct TimerView: View {
//every Second change Circle and Value (Circle is small because of the animation)
let timerForText = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let timerForCircle = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
#State var progress : Double = 1.0
#State var timerSeconds : Int = 60
#State var endDate: Date? = nil
let customInverval : Int
var body: some View {
ZStack{
ProgressCircleView(progress: self.$progress, timerSeconds: self.$timerSeconds, customInterval: customInverval)
.padding()
.onReceive(timerForCircle){ _ in
decreaseProgress()
}
VStack{
Text("\(timerSeconds)")
.font(.system(size: 80))
.bold()
.onReceive(timerForText){ _ in
incrementTimer()
}.multilineTextAlignment(.center)
}
.accessibilityElement(children: .combine)
}.padding()
.onAppear {
endDate = Date(timeIntervalSinceNow: TimeInterval(customInverval))
}
}
func decreaseProgress() -> Void {
guard let endDate = endDate else { return}
progress = max(0, endDate.timeIntervalSinceNow / TimeInterval(customInverval))
if endDate.timeIntervalSinceNow <= 0 {
timerForCircle.upstream.connect().cancel()
}
}
func incrementTimer() -> Void {
guard let endDate = endDate else { return}
timerSeconds = max(0, Int(endDate.timeIntervalSinceNow.rounded()))
if endDate.timeIntervalSinceNow <= 0 {
timerForText.upstream.connect().cancel()
print("stop")
}
}
}