Search code examples
iosswifttimer

Stopwatch not synced


Sorry if this is a newbie question, I am very new to iOS & Swift. I have a problem with the timer interval: I set 0.01 time interval but it doesn't correspond with the timer label, because 0.01 corresponds in one millisecond but it doesn't show it. So basically the timer is skewed.

timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateStopwatch) , userInfo: nil, repeats: true)

@IBAction func startStopButton(_ sender: Any) {
    buttonTapped()
}


func updateStopwatch() {
    milliseconds += 1
    if milliseconds == 100 {
        seconds += 1
        milliseconds = 0
    }
    if seconds == 60 {
        minutes += 1
        seconds = 0
    }
    let millisecondsString = milliseconds > 9 ?"\(milliseconds)" : "0\(milliseconds)"
    let secondsString = seconds > 9 ?"\(seconds)" : "0\(seconds)"
    let minutesString = minutes > 9 ?"\(minutes)" : "0\(minutes)"
    stopWatchString = "\(minutesString):\(secondsString).\(millisecondsString)"
    labelTimer.text = stopWatchString
}

func buttonTapped() {
    if isTimerRunning {
        isTimerRunning = !isTimerRunning
        timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateStopwatch) , userInfo: nil, repeats: true)
        startStopButton.setTitle("Stop", for: .normal)
    }else{
        isTimerRunning = !isTimerRunning
        timer.invalidate()
        startStopButton.setTitle("Start", for: .normal)
    }
}

Solution

  • Devices have maximum screen update rate (most are 60 fps), so there is no point in going faster than that. For maximum screen refresh rate, use a CADisplayLink rather than a Timer, which is coordinated perfectly for screen refreshes (not only in frequency, but also the timing within the screen refresh cycle).

    Also don't try to keep track of the time elapsed by adding some value (because you are not guaranteed that it will be called with the desired frequency). Instead, before you start your timer/displaylink, save the start time and then when the timer/displaylink is called, display the elapsed time in the desired format.

    For example:

    var startTime: CFTimeInterval!
    weak var displayLink: CADisplayLink?
    
    func startDisplayLink() {
        self.displayLink?.invalidate()  // stop prior display link, if any
        startTime = CACurrentMediaTime()
        let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
        displayLink.add(to: .current, forMode: .commonModes)
        self.displayLink = displayLink
    }
    
    func handleDisplayLink(_ displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        let minutes = Int(elapsed / 60)
        let seconds = elapsed - CFTimeInterval(minutes) * 60
        let string = String(format: "%02d:%05.2f", minutes, seconds)
    
        labelTimer.text = string
    }
    
    func stopDisplayLink() {
        displayLink?.invalidate()
    }
    

    Note, CACurrentMediaTime() uses mach_time, like hotpaw2 correctly suggested, but does the conversion to seconds for you.