28

The following code snippet works perfectly when called outside a completion block, but the timer is never fired when I set it up inside the block. I don't understand why there is a difference:

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

I was not using the self references when calling it initially outside the block, but then once inside, it was required. However I tested the exact same code outside the block again and it does still work.

The block is a completion hander that is called after asking permission for HealthKit related information.

kvn
  • 1,295
  • 11
  • 28

3 Answers3

75

The issue is that the completion block in question was probably not running on the main thread and therefore didn't have a run loop. But a Timer needs to be scheduled on a run loop, and while the main thread has one, most background threads do not (unless you add one, yourself, which is an exceedingly rare pattern nowadays).

To fix this, in that completion handler, one could dispatch the creation of the timer back to the main thread, which already has a run loop:

DispatchQueue.main.async {
    self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
}

Alternatively, from that background thread, one can also add it to the main run loop:

let timer = Timer(timeInterval: 4.0, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .default)

Finally, if one really wanted the Timer to run on a background queue, one would reach for a GCD “dispatch source timer”. This is a timer that can be scheduled for a background queue, and doesn't require a run loop.

var timer: DispatchSourceTimer!

private func startTimer() {
    let queue = DispatchQueue(label: "com.domain.app.timer")
    timer = DispatchSource.makeTimerSource(queue: queue)
    timer.setEventHandler { [weak self] in
        // do something
    }
    timer.schedule(deadline: .now(), repeating: 1.0)
    timer.resume()
}

For syntax for earlier version of Swift, see previous revision of this answer.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks makes a lot of sense. I should have thought of that. Thanks. – kvn Jul 03 '16 at 01:21
  • Is there ever a reason to not use `DispatchSourceTimer` and only use timer? – mfaani Jun 18 '19 at 22:01
  • If running on the main queue, I often lean towards `Timer` for its simplicity. If running timer on background queue, though, dispatch timer is right tool. – Rob Jun 19 '19 at 03:07
12

Another reason why Timer() might not work work is how it's created. I had the same problem, and everything I tried didn't solve it, including instantiating on the main thread. I stared at this for quite a while until I realized (stupidly) that I was creating it differently. Instead of Timer.scheduledTimer

I instantiated it using

let timer = Timer(timeInterval: 4.0, target: self, selector: #selector(self.timerCompletion), userInfo: nil, repeats: true)

In my case I had to actually add it to a run loop to get it to run. Like this

RunLoop.main.add(timer, forMode: RunLoop.Mode.default)
mfaani
  • 33,269
  • 19
  • 164
  • 293
SafeFastExpressive
  • 3,637
  • 2
  • 32
  • 39
4

This may sound obvious, but I had a similar problem, the timer just wouldn't fire and the reason is that it wasn't in the main thread...No errors, just never fired.

Put in the main thread and at least you have a shot at it!

 DispatchQueue.main.async {
  //insert your timer here
}