Why would a `scheduledTimer` fire properly when setup outside a block, but not within a block?

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.

Leave a Comment