5

This timer isn't firing every second, when I check the log and UI it seems to be firing every 3-4 seconds.

func startTimer() {
    print("start timer")
    timer = Timer.scheduledTimer(timeInterval: 1,
                                 target: self,
                                 selector: #selector(timerDidFire),
                                 userInfo: nil,
                                 repeats: true)
}

func timerDidFire(timer: Timer) {
    print("timer")
    updateLabels()
}

Is this just something that is going to happen on the Watch due to lack of capabilities, or is there something wrong in my code?

Here is the log if needed:

0.0396000146865845
3.99404102563858
7.97501903772354
11.9065310359001

EDIT:

And for clarification, what I'm updating every second is the workout timer, so it needs to be updated every second that ticks by.

SRMR
  • 3,064
  • 6
  • 31
  • 59

3 Answers3

3

If your app is busy doing something else, which blocks or delays the run loop from checking that the fire time has repeatedly passed, the timer will only fire once during that period:

A repeating timer always schedules itself based on the scheduled firing time, as opposed to the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.

As an aside, it may be more efficient to update your UI based on a response to a change (e.g., observation), or reaction to an event (e.g., completion handler).

  • This avoids creating busy work for the app when it's driven to check yet doesn't actually have a UI update to perform, if nothing has changed during the timer interval.

  • It also prevents multiple changes within the fire interval from being ignored, since a timer-driven pattern would only be displaying the last change in the UI.

  • Thanks for the response! I think that makes sense. But I guess my only question is that I'm just updating the UI for the label that shows the time elapsed in a workout, so that needs to be updated every second, and I'm using the same method as was shown in the WWDC 2016 video and documentation for this exact process. You know what I mean? – SRMR Aug 14 '16 at 02:04
  • Ah. You should edit your question and describe what you're specifically trying to do. Based on [your previous question](http://stackoverflow.com/q/38937657/4151918), I figured you were updating calories. –  Aug 14 '16 at 02:10
  • Ah, got it. I edited the question to describe more specifics, so thanks. Do you have any ideas on it now that you have the specifics that it is time elapsed I'm updating? – SRMR Aug 14 '16 at 13:17
  • Isn't this what WKInterfaceTimer is for? Which WWDC video? – ncke Aug 21 '16 at 15:46
1

Consider using a WKInterfaceTimer label in place of the label that you are using to show the timing:

A WKInterfaceTimer object is a special type of label that displays a countdown or count-up timer. Use a timer object to configure the amount of time and the appearance of the timer text. When you start the timer, WatchKit updates the displayed text automatically on the user’s Apple Watch without further interactions from your extension. Apple Docs.

WatchOS will then take responsibility for keeping this up-to-date. The OS handles the label for you, but you have to keep track of the elapsed time: you just need to set an NSDate to do that (see example below).

Sample Code.

In your WKInterfaceController subclass:


    // Hook up a reference to the timer.
    @IBOutlet var workoutTimer: WKInterfaceTimer!

    // Keep track of the time the workout started.
    var workoutStartTime: NSDate?

    func startWorkout() {
        // To count up use 0.0 or less, otherwise the timer counts down.
        workoutTimer.setDate(NSDate(timeIntervalSinceNow: 0.0))
        workoutTimer.start()
        self.workoutStartTime = NSDate()
    }

    func stopWorkout() {
        workoutTimer.stop()
    }

    func workoutSecondsElapsed() -> NSTimeInterval? {
        // If the timer hasn't been started then return nil
        guard let startTime = self.workoutStartTime else {
            return nil
        }
        // Time intervals from past dates are negative, so
        // multiply by -1 to get the elapsed time.
        return -1.0 * self.startTime.timeIntervalSinceNow
    }

Comprehensive blog entry: here.

ncke
  • 1,084
  • 9
  • 14
1

As of 2021, the (Foundation) Timer object supports a tolerance variable (measured in seconds). Set timer.tolerance = 0.2, and you should get a fire every second (+/- 0.2 seconds). If you are just updating your GUI, the exact time interval isn't that critical, but this should be more reliable than using no tolerance value. You'll need to create the timer separately, and manually add to the run queue such as below... (Swift)

import Foundation

// Set up timer to fire every second
let newTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {timer in
    self.timerFired()
}
newTimer.tolerance = 0.2 // For visual updates, 0.2 is close enough
RunLoop.current.add(newTimer, forMode: .common)
John Robi
  • 345
  • 2
  • 8