UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

The ultimate guide to Timer

How to schedule timers, repeat timers, and more

Paul Hudson       @twostraws

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.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Creating a repeating timer

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.

Creating a non-repeating timer

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!")
}

Ending a timer

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()
    }
}

Attaching context

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

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:

  • After 1.0 seconds the timer fires.
  • After 2.4 seconds the timer fires again. It’s 0.4 seconds late, but that’s still within our tolerance.
  • After 3.1 seconds the timer fires again. This is only 0.7 seconds after our previous fire event, but each fire date is calculated from the original regardless of tolerance.
  • After 4.5 seconds the timer fires again.
  • And so on…

Working with runloops

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)

Synchronizing your timer with screen updates

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.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.5/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.