Search code examples
iosswifttimer

How to keep two timers in sync


I'm still pretty new to coding and Swift. So bear with me.

Problem Statement : I've got a stopwatch style app that has two concurrent timers start at the same time and display in a mm:ss.SS format, but one is designed to reset to 0 at specific intervals automatically while the other keeps going and tracks total time.

Similar to a "lap" function but it does it automatically. The problem I've encountered is that occasionally the timers aren't perfectly synced up when the user pauses the timers. Since the reset happens at an exact second, both timers should have identical hundredths of a second, while the seconds and minutes will obviously be different. But sometimes the hundredths will be off by .01 or more.

Now, I know Timer isn't designed to be perfectly accurate, and in practice on my app this isn't even a huge deal. My timer doesn't even need to be accurate to the hundredth of a second, and while running it's not noticeably off at all, only while paused. I could display fewer decimal places or none at all, but I prefer the style of showing the hundredths since it fits in well with the stock timer app style.

So if there's a way to make this work, I'd like to keep it.

Screenshot : screenshot

What I tried :

@IBAction func playPauseTapped(_ sender: Any) {
        if timerState == .new {
            //start new timer
            startCurrentTimer()
            startTotalTimer()
            currentStartTime = Date.timeIntervalSinceReferenceDate
            totalStartTime = Date.timeIntervalSinceReferenceDate
            timerState = .running

            //some ui updates
        } else if timerState == .running {
            //pause timer
            totalTimer.invalidate()
            currentTimer.invalidate()
            timerState = .paused
            pausedTime = Date()
            //other ui updates
        } else if timerState == .paused {
            //resume paused timer
            let pausedInterval = Date().timeIntervalSince(pausedTime!)
            pausedIntervals.append(pausedInterval)
            pausedIntervalsCurrent.append(pausedInterval)
            pausedTime = nil
            startCurrentTimer()
            startTotalTimer()
            timerState = .running
            //other ui updates
        }

    }

    func startTotalTimer() {
        totalTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runTotalTimer), userInfo: nil, repeats: true)
    }

    func startCurrentTimer() {
        currentTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runCurrentTimer), userInfo: nil, repeats: true)
    }

    func resetCurrentTimer() {
        currentTimer.invalidate()
        currentStartTime = Date.timeIntervalSinceReferenceDate
        pausedIntervalsCurrent.removeAll()
        startCurrentTimer()
    }

    @objc func runCurrentTimer() {
        let currentTime = Date.timeIntervalSinceReferenceDate

        //calculate total paused time
        var pausedSeconds = pausedIntervalsCurrent.reduce(0) { $0 + $1 }
        if let pausedTime = pausedTime {
            pausedSeconds += Date().timeIntervalSince(pausedTime)
        }

        let currentElapsedTime: TimeInterval = currentTime - currentStartTime - pausedSeconds

        currentStepTimeLabel.text = format(time: currentElapsedTime)

        if currentElapsedTime >= recipeInterval {
            if recipeIndex < recipeTime.count - 1 {
                recipeIndex += 1

                //ui updates

                //reset timer to 0
                resetCurrentTimer()
            } else {
                //last step
                currentTimer.invalidate()
            }

        }
    }

    @objc func runTotalTimer() {
        let currentTime = Date.timeIntervalSinceReferenceDate

        //calculate total paused time
        var pausedSeconds = pausedIntervals.reduce(0) { $0 + $1 }
        if let pausedTime = pausedTime {
            pausedSeconds += Date().timeIntervalSince(pausedTime)
        }

        let totalElapsedTime: TimeInterval = currentTime - totalStartTime - pausedSeconds

        totalTimeLabel.text = format(time: totalElapsedTime)

        if totalElapsedTime >= recipeTotalTime {
            totalTimer.invalidate()
            currentTimer.invalidate()
            //ui updates
        }
    }

    func format(time: TimeInterval) -> String {
        //formats TimeInterval into mm:ss.SS

        let formater = DateFormatter()
        formater.dateFormat = "mm:ss.SS"

        let date = Date(timeIntervalSinceReferenceDate: time)
        return formater.string(from: date)
    }

Solution

  • You should use a single timer. And when you need a reset to zero, save the current time to a variable.

    When presenting the time in the UI, calculate the difference between the running total timer, and the time you saved previously.