How to schedule timers, repeat timers, and more
Swift’s Timer
class is a flexible way to schedule work to happen in the future, either just once or repeatedly. In this guide I will to provide a selection of ways to work with it, along with solutions for common problems.
Note: Before I start, I want to make it clear that there is a significant energy cost to using timers. We’ll look at ways to mitigate this, but broadly any kind of timer must wake the system from its idle state in order to trigger work, and that has an associated energy cost.
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Let’s start with the basics. You can create and start a repeating timer to call a method like this:
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
You’ll need an fireTimer()
method for it to call, so here’s a simple one just for testing:
@objc func fireTimer() {
print("Timer fired!")
}
Note: That needs to use @objc
because Timer
uses the target/action approach to method calls.
Although we’ve requested the timer be triggered every 1.0 seconds, iOS reserves the right to be a little flexible about that timing – it is extremely unlikely that your method will be triggered at precisely one-second intervals.
Another common way to create a repeating timer is using a closure, like this:
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Timer fired!")
}
Both of these initializers return the timer that was created. You don’t need to store these in a property, but it’s generally a good idea so that you can terminate the timer later. Because the closure approach gets passed the timer each time your code runs, you can invalidate it from there if you wish.
If you want code to run only once, change repeats: true
to repeats: false
, like this:
let timer1 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false)
let timer2 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
print("Timer fired!")
}
The rest of your code is unaffected.
Although this is approach works fine, personally I prefer to use GCD to accomplish the same:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("Timer fired!")
}
You can destroy an existing timer by calling its invalidate()
method. For example, this code creates a timer that prints “Timer fired!” three times, once a second, then terminates it:
var runCount = 0
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Timer fired!")
runCount += 1
if runCount == 3 {
timer.invalidate()
}
}
To do the same thing with a method, you’d first need to declare timer
and runCount
as properties:
var timer: Timer?
var runCount = 0
Next, schedule the timer at some point:
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
Finally, fill in your fireTimer()
method with whatever you need:
@objc func fireTimer() {
print("Timer fired!")
runCount += 1
if runCount == 3 {
timer?.invalidate()
}
}
Alternatively, you can do without the timer
property by making fireTimer()
accept the timer as its parameter. This will automatically be passed if you ask for it, so you could rewrite fireTimer()
to this:
@objc func fireTimer(timer: Timer) {
print("Timer fired!")
runCount += 1
if runCount == 3 {
timer.invalidate()
}
}
When you create a timer to execute a method, you can attach some context that stores extra information about what triggered the timer. This is a dictionary, so you can store pretty much any data you like – the event that triggered the timer, what the user was doing, what table view cell was selected, and so on.
For example, we could pass in a dictionary containing a username:
let context = ["user": "@twostraws"]
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true)
We could then read that inside fireTimer()
by looking at the userInfo
property of the timer parameter:
@objc func fireTimer(timer: Timer) {
guard let context = timer.userInfo as? [String: String] else { return }
let user = context["user", default: "Anonymous"]
print("Timer fired by \(user)!")
runCount += 1
if runCount == 3 {
timer.invalidate()
}
}
Adding some tolerance to your timer is an easy way to reduce its energy impact. It allows you specify some leeway for the system when it comes to executing your timer: “I’d like for this to be run once a second, but if it’s 200 milliseconds late I won’t be upset.” This allows the system to perform timer coalescing, which is a fancy term that means it can combine multiple timers events together to save battery life.
When you specify tolerance, you’re saying that the system can trigger your timer at any point between your original request and that time plus your tolerance. For example, if you ask for the timer to be run after 1 second with a tolerance of 0.5 seconds, it might be executed after 1 second, 1.5 seconds, 1.3 seconds, and so on. However, the timer will never be executed before you ask it – tolerance adds time after your requested execution date.
This example creates a timer to run every 1 second, with 0.2 seconds of tolerance:
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
timer.tolerance = 0.2
The default tolerance is 0, but remember that the system automatically adds a little tolerance.
If your repeating timer is executed a little late thanks to the tolerance you specified, that doesn’t mean it will continue executing late. iOS won’t allow your timer to drift, which means the next trigger might happen more quickly.
As an example, consider a timer that was asked to execute every 1 second with a 0.5 second tolerance. It might run like this:
One common problem folks hit when using timers is that they won’t fire when the user is interacting with your app. For example, if the user has their finger touching the screen so they can scroll through a table view, your regular timers won’t get fired.
This happens because we’re implicitly creating our timer on the defaultRunLoopMode
, which is effectively the main thread of our application. This will then get paused while the user is actively interacting with our UI, then reactivated when they stop.
The easiest solution is to create the timer without scheduling it directly, then add it by hand to a runloop of your choosing. In this case, .common
is the one we want: it allows our timers to fire even when the UI is being used.
For example:
let context = ["user": "@twostraws"]
let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: context, repeats: true)
RunLoop.current.add(timer, forMode: .common)
Some people, particularly those making games, try to use timers to have some work done before every frame is drawn – i.e., 60 or 120 frames per second, depending on your device.
This is a mistake: timers are not designed for that level of accuracy, and you have no way of knowing how much time has elapsed since the last frame was drawn. So, you might think you have 1/60th or 1/120th of a second to run your code, but in practice half of that might already have passed before your timer was triggered.
So, if you want to have some code run immediately after the previous display update, you should use CADisplayLink
instead. I’ve written some example code for that already (see How to synchronize code to drawing using CADisplayLink), but here’s a quick snippet:
let displayLink = CADisplayLink(target: self, selector: #selector(fireTimer))
displayLink.add(to: .current, forMode: .default)
As with firing timers, if you want your display link method to trigger even when the UI is currently being used, make sure you specify .common
rather than .default
.
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.