A Background Repeating Timer in Swift

Daniel Galasko
Over Engineering
Published in
3 min readAug 27, 2017

Background timers are a very usefool tool in one’s developer toolbox. Most of the time when we want to schedule a repeating unit of work we consult NSTimer . Problem is, NSTimer requires an active run loop which is not always readily available on background queues. The main thread has an active run loop but this defeats the purpose of having our timer run in the background so its a definite no go. So, to get a dedicated background-queue-friendly timer, we use GCD.

Hello DispatchSourceTimer 👋

Like most APIs we deal with from Apple, there is always a lower level counterpart. When it comes to background timers, we have DispatchSourceTimer . The code to construct one is rather simple:

let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now(), interval: .seconds(1))
t.setEventHandler(handler: { [weak self] in
// called every so often by the interval we defined above
})

This will create a repeating timer that will fire events on a default background queue unless one is specified in makeTimerSource(). All we need to do is create one and we have a repeating background timer 🚀…right?

Not So Fast 🐒

Unfortunately, it’s not that easy. Whilst that code will create a timer, you will experience crashes if you ever try deallocating that timer, or if you are looking to implement pause and resume functionality. Lets fix that 👩‍⚕️

Pausing and Resuming

GCD timers can be somewhat sensitive. If you try and resume/suspend an already resumed/suspended timer, you will get a crash with a reason like:

BUG IN CLIENT OF LIBDISPATCH: Over-resume of an object

This tells us we have tried to resume an already resumed timer 💩. Luckily the fix is simple, we just need to balance the calls to suspend and resume, like the documentation states:

As a result, you must balance each call to dispatch_suspend with a matching call to dispatch_resume before event delivery resumes.

Now is a good time to create a safe wrapper to use these timers

class RepeatingTimer {    let timeInterval: TimeInterval    init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)? private enum State {
case suspended
case resumed
}
private var state: State = .suspended func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}

func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
}

This allows us to have a timer that will ensure not to over resume/suspend itself. We should also ensure its accessed from the same thread/queue otherwise we will need to add an internal serial queue to prevent race conditions.

Preventing Deallocation Crashes 💥

One final hurdle before our timer is complete is to ensure it can be deallocated properly. To deallocate a timer, it needs to be cancelled. If this is not done, GCD will trigger the timer and call the event handler on a deallocated object and 💥. We don’t want this. Furthermore, we also don’t want to cancel a timer that has been suspended(paused) because this will also trigger a crash. Unfortunately, this isn’t really documented but I did find a post in the Developer Forums that outlined how to ensure timers don’t crash when they are cancelled and deinitialised.

Our deinitialisation function can now be written as:

deinit {
timer.setEventHandler {}
timer.cancel()
/*
If the timer is suspended, calling cancel without resuming
triggers a crash. This is documented here
https://forums.developer.apple.com/thread/15902
*/
resume()
eventHandler = nil
}

And Finally 🏁

Now that we have our timer up and running, using it is simple:

let t = RepeatingTimer(timeInterval: 3)
t.eventHandler = {
print("Timer Fired")
}
t.resume()

This timer will automatically fire events on a background queue. The reasoning for this is that DispatchSource.makeTimerSource() creates a timer that uses a default background operation queue as you will see in the documentation:

class func makeTimerSource(flags: DispatchSource.TimerFlags = default, queue: DispatchQueue? = default) -> DispatchSourceTimer

If you want to configure the queue it uses I leave that up to you 👍

I included the full timer code in a gist that can be found below. If you won’t be using your timer on a dedicated background serial queue you should definitely include a serial queue inside it to ensure you don’t have any race conditions.

Happy coding 🤸‍♂️

--

--

Daniel Galasko
Over Engineering

"If you only do what you can do, you'll never be better than what you are" - Kung Fu Panda 3. Currently iOS @GoDaddy