How To: Working With Timers In Swift

Written by Reinder de Vries on October 23 2018 in App Development

How To: Working With Timers In Swift

Timers are super handy in Swift, from creating repeating tasks to scheduling work with a delay. This article explains how to create a timer in Swift.

This article shows you how to use the Timer class, formerly known as NSTimer, to schedule timers. We’ll get into repeating and non-repeating timers, using run loops, keeping track of timers, and how you can reduce their energy and power impact.

Ready? Let’s go.

  1. How To Create A Repeating Timer
  2. Non-Repeating Timers And Managing Timers
  3. Creating A Countdown Timer
  4. Timers, Runloops and Tolerance
  5. Executing Code With A Delay
  6. Further Reading

How To Create A Repeating Timer

Creating a repeating timer in Swift is surprisingly simple. Here’s how:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fire), userInfo: nil, repeats: true)

@objc func fire() 
{
    print("FIRE!!!")
}

In the above code, a few things happen:

  • A timer is created using the Timer.scheduledTimer(...) class method. Its return value is assigned to the constant timer. This constant now contains a reference to the timer, which will come in handy later on.
  • The parameters of scheduledTimer() are the timer interval of 1 second, which uses a mechanism known as target-action, some userInfo that’s set to nil, and the parameter repeats set to true.
  • We’ve also coded a function fire(). This is the function that’s called when the timer fires, i.e. roughly every second. By setting target to self and selector to #selector(fire) you’re indicating that whenever the timer fires, the function fire() of self needs to be called.

The above code should run in a class context, for instance in a view controller class. The fire() function is part of the class, and self refers to the current class instance.

You can also create a timer using a closure. Like this:

let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timer in
    print("FIRE!!!")
})

In the above code, the last parameter block takes a closure. The closure has one parameter timer itself.

The @objc attribute makes the fire() function available in Objective-C. The Timer class is part of the Objective-C runtime, so that’s why we need that @objc attribute.

Learn how to build iOS apps

Get started with iOS 12 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 5 and Xcode 10.

Non-Repeating Timers And Managing Timers

In the previous example, we’ve used 5 parameters to create a timer. They are:

  • timeInterval: the interval between timer fires in seconds, type is Double
  • target: a class instance that the function for selector should be called on
  • selector: the function to call when the timer fires, with #selector(...)
  • userInfo: a dictionary with data that’s provided to the selector, or nil
  • repeats: whether this timer is repeating or non-repeating

Creating a non-repeating timer is as simple as setting the repeats parameter to false. The timer will only fire once and immediately invalidate itself after.

Then how do you stop a repeating timer? It’s simple. Here’s how:

timer.invalidate()

You’d keep track of the timer variable somewhere, for instance through a property on the class that you’re using the timer in.

You can also attach some extra information to a timer by using userInfo. This value is sent to the function that fires, via its timer parameter. Here’s an example:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fire(timer:)), userInfo: ["score": 10], repeats: true)

@objc func fire(timer: Timer) 
{
    if  let userInfo = timer.userInfo as? [String: Int],
        let score = userInfo["score"] {

        print("You scored \(score) points!")
    }
}

In the above code, we’ve added the dictionary ["score": 10] to the timer. When it fire, this dictionary is provided to the fire(timer:) function as its timer parameter. Inside the fire(timer:) function we’re checking if the userInfo property has the type we expect, and we get the right value.

Let’s look at a practical example for using timers, next.

Creating A Countdown Timer

Imagine you’re creating a game. The user has 60 seconds to solve a puzzle and score points. As the count down timer runs out, you’re keeping track of how many seconds are left.

First, we’re creating two properties at the top of our game class. Something like:

var timer:Timer?
var timeLeft = 60

The timer doesn’t start immediately. Until it starts, the timer property is nil. At some point the game has started, and we start the timer:

timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(onTimerFires), userInfo: nil, repeats: true)

The timer calls onTimerFires() every second, indefinitely. And here’s that function:

@objc func onTimerFires()
{
    timeLeft -= 1
    timeLabel.text = "\(timeLeft) seconds left"

    if timeLeft <= 0 {
        timer.invalidate()
        timer = nil
    }
}

Every time the timer fires it subtracts 1 from timeLeft, and it updates the “time left label”. When the timer reaches zero, timer is invalidated and set to nil.

This will effectively create a countdown timer that counts down from 60 to zero. And at a point in the future, you can of course reset the countdown and start again.

Timers, Runloops and Tolerance

Timers work in conjunction with run loops. Run loops are a fundamental part of threads and concurrency.

It’s easiest to imagine run loops like a ferris wheel. People can enter a passenger car and get moved around by the wheel. In a similar way, you can schedule a task on a run loop. The run loop keeps “looping” and executing tasks. When there are no tasks to execute, the run loop waits or quits.

The way run loops and timers work together, is that the run loop checks if a timer should fire. A timer isn’t a real-time mechanism, because the firing of the timer can coincide with the runloop executing a task. You can compare this to wanting to enter the ferris wheel when there’s no car yet at the bottom entrance. The result is that a timer can fire later than it’s scheduled.

This isn’t necessarily a bad thing. Don’t forget you’re running code on a mobile device that has energy and power constraints! The device can schedule run loops more efficiently to save power.

You can also help the system save power by using a timer property called tolerance. This will tell the scheduling system: “Look, I want this to run every second, but I don’t care if it’s 0.2 seconds too late.” This of course depends on your app. Apple recommends to set the tolerance to at least 10% of the interval time for a repeating timer.

Here’s an example:

let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fire), userInfo: nil, repeats: true)
timer.tolerance = 0.2

The tolerance property uses the same units as the timeInterval property, so the above code will use a tolerance of 200 milliseconds.

The tolerance will never cause a timer to fire early, only later. And the tolerance will neither cause a timer to “drift”, i.e. when one timer fire is too late, it won’t affect the scheduled time of the next timer fire.

When using the Timer.scheduledTimer(...) class method, the timer is automatically scheduled on the current run loop in the default mode. This is typically the run loop of the main thread. As a result, timers may not fire when the run loop on the main thread is busy, for instance when the user of your app is interacting with the UI.

You can solve this by manually scheduling the timer on a run loop yourself. Here’s how:

let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(fire), userInfo: nil, repeats: true)

RunLoop.current.add(timer, forMode: .commonModes)

The first line of code will create an instance of Timer using the Timer(...) initializer. The second line of code adds the timer to the current run loop using the .commonModes input mode. In short, this tells the run loop that the timer should be checked for all “common” input modes, which effectively lets the timer fire during the interaction with UI.

By the way, a common source for frustration is accidentally using the Timer(...) initializer when you wanted to use the Timer.scheduledTimer(...) to create a timer. If you use the former, the timer won’t fire until you’ve added it to a run loop! And you’ll pull your hair out, because you’re certain you’ve scheduled the timer correctly…

Executing Code With A Delay

A common scenario in practical iOS development is executing some code with a small delay. It’s easiest to use Grand Central Dispatch for that purpose, and not use a Timer.

The following code executes a task on the main thread with a 300 millisecond delay:

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {  
    print("BOOYAH!")
}

It’s more concise than using Timer. And because you’re only running it once, there’s no need to keep track of a timer anyway.

Learn how to build iOS apps

Get started with iOS 12 and Swift 5

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 5 and Xcode 10.

Further Reading

So, now you know! When you need your code run in a timely manner, Timer is a tool you can depend on. Give or take ;-)

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.