iOS Timer Tutorial

In this iOS Timer tutorial, you’ll learn how timers work, affect UI responsiveness and battery and how to work with animations using CADisplayLink. By Fabrizio Brancati.

Leave a rating/review
Download materials
Save for later
Share

Imagine that you’re working on an app where you need a certain action to be triggered in the future – maybe even repeatedly. It would be helpful to have a concept in Swift that offers this functionality, right? That’s exactly where Swift’s Timer class comes into the picture.

It is common to use Timer to schedule things in your app. This could be, for example, a one-time event or something that happens periodically.

In this tutorial, you’ll learn exactly how Timer works in iOS, how it affects the UI responsiveness, how to improve device power usage working with Timer and how to use CADisplayLink for animations.

This tutorial walks you through building a ToDo app. The ToDo app tracks the progression of time for a task until a task is completed. Once the task is completed, the app congratulates the user with a simple animation.

Ready to explore the magic world of timers in iOS? Time to dive in!

Getting Started

Download the project materials using the Download Materials button at the top or bottom of this tutorial.

Open the starter project and check out the project files. Build and run. You’ll see a simple ToDo app:

First, you’ll create a new task in your app. Tap the + tab bar button to add a new task. Enter a name for your task (e.g., “Buy Groceries”). Tap OK. Great!

Added tasks will reflect a time signature. The new task you created is labeled zero seconds. You’ll note that the seconds label does not increment at the moment.

In addition to adding tasks, you can mark them as complete. Tap on the task you created. Doing so crosses out a task name and labels the task as completed.

Creating Your First Timer

For your first task, you’ll create the app’s main timer. As mentioned above, Swift’s Timer class, otherwise known as NSTimer, is a flexible way to schedule work to happen at some point in the future, either periodically or just once.

Open TaskListViewController.swift and add the following variable to TaskListViewController:

var timer: Timer?

Then, declare an extension at the bottom of TaskListViewController.swift:

// MARK: - Timer
extension TaskListViewController {

}

And add the following code to the TaskListViewController extension:

@objc func updateTimer() {
  // 1
  guard let visibleRowsIndexPaths = tableView.indexPathsForVisibleRows else {
    return
  }

  for indexPath in visibleRowsIndexPaths {
    // 2
    if let cell = tableView.cellForRow(at: indexPath) as? TaskTableViewCell {
      cell.updateTime()
    }
  }
}

This method:

  1. Checks if there is any visible row in tableView containing tasks.
  2. Calls updateTime for every visible cell. This method updates the cell time. Have a look at what it does in the TaskTableViewCell.swift file.

Now, add the following code to the TaskListViewController extension:

func createTimer() {
  // 1
  if timer == nil {
    // 2
    timer = Timer.scheduledTimer(timeInterval: 1.0,
                                 target: self,
                                 selector: #selector(updateTimer),
                                 userInfo: nil,
                                 repeats: true)
  }
}

Here, you:

  1. Check if timer contains an instance of a Timer.
  2. If not, set timer to a repeating Timer that calls updateTimer() every second.

Next, you need to create a timer whenever a user adds a task. To do this, call the following method as the first line of code in presentAlertController(_:):

createTimer()

Build and run your app.

To test your work, create a couple of new tasks using the same steps as before.

You’ll note that the table view cell’s time label now updates the elapsed time every second.

Adding Timer Tolerance

Increasing the number of timers in your app increases the risk for less app responsiveness and more power usage. Every Timer tries to fire itself at a precise one-second mark each second. This is because a Timer‘s default tolerance value is zero.

Adding tolerance to a Timer is an easy way to reduce the energy impact it has on your app. It lets the system fire the timer any time between the scheduled fire date and the scheduled fire date plus tolerance time — never before the scheduled fire date.

For repeating timers, the system calculates the next fire date from the original fire date, ignoring the tolerance applied at individual fire times. This is to avoid time drift.

To avoid any side effects of decreased app responsiveness and increased power usage in your app, you can set the timer property’s tolerance to 0.1.

In createTimer(), right after setting the timer to a Timer, add this line of code:

timer?.tolerance = 0.1

Build and run.

The changes may not be visually obvious. However, your users will benefit from the app responsiveness and power efficiency.

Trying Out Timers in the Background

You may wonder what happens to a timer when your app goes to the background.

To investigate this, add the following code as the first line of updateTimer():

if let fireDateDescription = timer?.fireDate.description {
  print(fireDateDescription)
}

This lets you see when a Timer fires from the Console.

Build and run. Next, add a task as you have before. Return to your device home screen, then open the ToDo app again.

You will see something similar to this in the Console:

As you can see, when your app enters the background, iOS pauses any running timers. And when the app enters the foreground again, iOS resumes the timers.

Understanding Run Loops

A run loop is an event-processing loop that schedules work and manages the receiving of incoming event. A run loop keeps a thread busy when there is work, and it puts a thread to sleep when there is an absence of work.

Every time you launch your app on iOS, the system creates a Thread — the main thread. Each Thread has a RunLoop automatically created for it as needed.

But why is this relevant for you? Currently, each Timer fires on the main thread and is attached to a RunLoop. As you may know, the main thread is in charge of drawing the user interface, listening for touches and such. When the main thread is busy with other things your app’s UI may become unresponsive and behave unexpectedly.

Did you notice that the task cell’s elapsed time label pauses when you drag the table view? There is a lag in which the timer does not fire when you scroll the table view.

A solution to this problem is to set the RunLoop to run timers with a different mode. More on this next!