Working with Timers in Swift

Written by Reinder de Vries on August 5 2020 in App Development, iOS

Working with Timers in Swift

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

We’ll discuss 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 with Swift
  2. Managing Timers and Non-Repeating 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 with Swift

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, the timer itself.

The timer with target-action and self won’t work in an Xcode playground, unless you create a class or struct. Here’s the code you can use to create a timer in a playground in Xcode:

import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

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

In the above code, we’re setting needsIndefiniteExecution to true so the playground won’t automatically stop running when the playground is finished. The timer is able to fire this way, and at that point we call finishExecution() to halt the playground.

Thanks to trailing-closure syntax we can make that closure-based timer even more concise. Like this:

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

See how the last argument label is omitted? That’s because the last parameter is a closure. It makes the code more concise. Neat!

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 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Managing Timers and Non-Repeating Timers

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

  • timeInterval: the interval between timer fires in seconds, its type is Double
  • target: a class instance that the function for selector should be called on, often self
  • 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 stored property on the class that you’re using the timer in. You can then use that property to manage the timer.

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 fires, this dictionary is passed to the fire(timer:) function as timer.userInfo. 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, so we also 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 already executing another 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 example 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 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

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

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

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.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy