27

I can't figure out how to make dispatch timer work repeatedly in Swift 3.0. My code:

let queue = DispatchQueue(label: "com.firm.app.timer",
                          attributes: DispatchQueue.Attributes.concurrent)
let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: UInt(0)),
                                           queue: queue)

timer.scheduleRepeating(deadline: DispatchTime.now(),
                        interval: .seconds(5),
                        leeway: .seconds(1)
)

timer.setEventHandler(handler: {
     //a bunch of code here
})

timer.resume()

Timer just fires one time and doesn't repeat itself like it should be. How can I fix this?

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Derreck
  • 383
  • 1
  • 4
  • 10

1 Answers1

66

Make sure the timer doesn't fall out of scope. Unlike Timer (where the RunLoop on which you schedule it keeps the strong reference until the Timer is invalidated), you need to maintain your own strong reference to your GCD timers, e.g.:

private var timer: DispatchSourceTimer?

private func startTimer() {
    let queue = DispatchQueue(label: "com.firm.app.timer", attributes: .concurrent)

    timer = DispatchSource.makeTimerSource(queue: queue)

    timer?.setEventHandler { [weak self] in // `[weak self]` only needed if you reference `self` in this closure and you want to prevent strong reference cycle
        print(Date())
    }

    timer?.schedule(deadline: .now(), repeating: .seconds(5), leeway: .milliseconds(100))

    timer?.resume()
}

private func stopTimer() {
    timer = nil
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thank you sir for guide me in the right direction! The queue and its timer were declared in a function inside IBAction func and they apparently fall out of scope after its first execution. Strange thing is they were declared exactly the same inside this internal func in Swift 2.3 and still worked like a charm. Anyway, thanks, now it works - and I'll also try to implement your solution tomorrow) – Derreck Sep 19 '16 at 20:06
  • 1
    Hmm, I'm surprised by the Swift 2.3 behavior, because this need to maintain your own strong reference has been longstanding behavior of dispatch source timers, long predating Swift. Regardless, I'm glad it seems to have solved your issue! – Rob Sep 19 '16 at 20:10
  • @Rob I call `stopTimer()` function, but my timer continues to count down forever. Any reason why it behaves this way? [My code is here](https://gist.github.com/bibscy/447c620cf3be55ce3f167722e955f482) – bibscy Dec 08 '16 at 16:24
  • 2
    @bibscy - The problem is that you're checking `timeToLive` only once, when you created the timer. You want to put that `if timeToLive <= 12 { timer.cancel() }` code inside the handler. – Rob Dec 09 '16 at 22:36
  • @Rob Hello there. The event handler is called on the queue the dispatch timer is running on? If I want to update the UI I would still need to call the main queue, right? – TheoK Jan 22 '17 at 20:31
  • 1
    Correct, that's the purpose of the queue parameter. Note, if running UI updates, you can also consider a `CADisplayLink`, a type of timer optimized for UI updates. – Rob Jan 22 '17 at 21:11
  • Thanks. One big advantage here is that this can go MUCH faster than even CADisplayLink (60fps is one fire every 17ms! This method can fire nearly every 2ms, in my testing on an iPhone 6+). Anyway: This is my NSTimer replacement using your code, mostly: https://gist.github.com/drosenstark/f86abf58a8c997d208392a1cce0bc903 – Dan Rosenstark Feb 22 '17 at 16:00
  • 1
    Dan, obviously, if the primary purpose of the timer is to update the UI with the greatest speed, then this faster GCD timer not only doesn't gain you anything (because the screen refresh is the constraining factor, and display links are optimally timed to match this), but the GCD timer is actually less efficient, wasting CPU cycles that can never be rendered on screen. But, clearly, if the purpose of the super-fast timer is for something other than UI updates, then definitely feel free to go GCD (recognizing the drain on the device if you do so). – Rob Feb 22 '17 at 16:43
  • Thanks Rob, agreed on all counts. Yeah, the goal is to send out MIDI faster, and update the screen as an afterthought. We'll see how it works out in practice shortly. – Dan Rosenstark May 04 '17 at 20:47
  • 1
    Cool. By the way, if you're decoupling UI updates from some other process that's going much faster, a dispatch source merge/add can be very useful. Do the faster processing on some other queue and then use dispatch source update the UI on the main queue. That avoids having the main queue getting unnecessarily back logged with UI updates. – Rob May 04 '17 at 20:53
  • @Rob the idiot's way to do it -- my way -- is to just dispatch async to the main dispatch queue... My guess is that the main queue does process all the updates, but it only shows the last one on the display (for a frame set or a rotation, for instance). Would you by chance have a link to a proper example with `dispatch_source_merge_data`? Thanks for the tip! – Dan Rosenstark May 05 '17 at 14:07
  • 1
    @DanRosenstark - No, I've discovered that sometimes the UI will actually get backlogged. For example, when doing a fast, yet long, process, the UI updates can come through faster than the main queue can handle them, and it will actually end up making it look much slower than it is. See http://stackoverflow.com/a/26744884/1271826 or http://stackoverflow.com/a/39949292/1271826 for "add" dispatch sources. The "merge" dispatch sources are the same basic idea. – Rob May 05 '17 at 15:08
  • timer?.setEventHandler { _ in } **error in Swift 4: " Ambiguous reference to member 'setEventHandler(qos:flags:handler:) ' "** – vipinsaini0 Oct 04 '17 at 06:20
  • 1
    @VipinSaini - Unlike `Timer`-based handler closure, the `DispatchTimerSource` handler doesn’t supply any parameter to the closure. So remove the `_ in`. – Rob Apr 13 '19 at 21:15