Search code examples
swiftswiftuitimercountdowntimer

How to make my timer more precise in SwiftUI?


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?

enter image description here

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
    }
}


Solution

  • 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")
            }
        }
    }