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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Utilizing Run Loop Modes

A run loop mode is a collection of input sources (such as screen touches or mouse clicks) and timers that can be observed, as well as a collection of run loop observers to be notified when events happen.

There are three run loop modes in iOS:

  1. default: Handles input sources that are not NSConnectionObjects.
  2. common: Handles a set of run loop modes for which you can define a set of sources, timers and observers.
  3. tracking: Handles the app’s responsive UI.

For the purposes of your app, the common run loop mode sounds like the best match. In order to use it, go to createTimer() and replace its contents with the following code:

if timer == nil {
  let timer = Timer(timeInterval: 1.0,
                    target: self,
                    selector: #selector(updateTimer),
                    userInfo: nil,
                    repeats: true)
  RunLoop.current.add(timer, forMode: .common)
  timer.tolerance = 0.1
  
  self.timer = timer
}

The main difference between this snippet and the previous code is that the new code adds the timer on the run loop in common mode before setting the TaskListViewController‘s timer.

Now, build and run!

Congratulations, your table view cells’ time labels are responsive even when you are scrolling the table view!

Adding a Task Completion Animation

Now, you’ll add a congratulations animation when your user completes all tasks.

You’ll create a custom animation — a balloon that goes from the bottom to the top of the screen!

Add the following variables to the top of TaskListViewController:

// 1
var animationTimer: Timer?
// 2
var startTime: TimeInterval?, endTime: TimeInterval?
// 3
let animationDuration = 3.0
// 4
var height: CGFloat = 0

These variables’ purposes are to:

  1. Handle the animation timer.
  2. Take care of the animation start time and end time.
  3. Specify the animation duration.
  4. Handle the animation height.

Now, add the following TaskListViewController extension code to the end of TaskListViewController.swift:

// MARK: - Animation
extension TaskListViewController {
  func showCongratulationAnimation() {
    // 1
    height = UIScreen.main.bounds.height + balloon.frame.size.height
    // 2
    balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2,
      y: height + balloon.frame.size.height / 2)
    balloon.isHidden = false

    // 3
    startTime = Date().timeIntervalSince1970
    endTime = animationDuration + startTime!

    // 4
    animationTimer = Timer.scheduledTimer(withTimeInterval: 1 / 60, 
      repeats: true) { timer in
      // TODO: Animation here
    }
  }
}

In the code above, you:

  1. Calculate the right height for the animation based on the device’s screen height.
  2. Center the balloon outside of the screen and set its visibility.
  3. Create the startTime and calculate the endTime by adding the animationDuration to the startTime.
  4. Start the animation timer and have it update the progress of the animation 60 times per second with a block-based Timer API.

Next, you need to create the logic for updating the congratulations animation. To do this, add the following code after showCongratulationAnimation():

func updateAnimation() {
  // 1
  guard
    let endTime = endTime,
    let startTime = startTime 
    else {
      return
  }

  // 2
  let now = Date().timeIntervalSince1970

  // 3
  if now >= endTime {
    animationTimer?.invalidate()
    balloon.isHidden = true
  }

  // 4
  let percentage = (now - startTime) * 100 / animationDuration
  let y = height - ((height + balloon.frame.height / 2) / 100 * 
    CGFloat(percentage))

  // 5
  balloon.center = CGPoint(x: balloon.center.x + 
    CGFloat.random(in: -0.5...0.5), y: y)
}

Here, you:

  1. Check that the endTime and startTime are not nil.
  2. Save the current time to a constant.
  3. Ensure that the current time has not passed the end time. And in case it has, invalidate the timer and hide the balloon.
  4. Calculate the animation percentage and the desired y-coordinate the balloon should be moved to.
  5. Set the balloon’s center position based on previous calculations.

Now, replace // TODO: Animation here in the showCongratulationAnimation() with this code:

self.updateAnimation()

updateAnimation() is now called every time the animation timer fires.

Congratulations, you have created a custom animation! However, nothing new happens when you build and run the app…

Showing the Animation

As you may have guessed, nothing is triggering your newly created animation at the moment. To fire it off, you need just one more method. Add this code in the TaskListViewController animation extension:

func showCongratulationsIfNeeded() {
  if taskList.filter({ !$0.completed }).count == 0 {
    showCongratulationAnimation()
  }
}

This will be called every time a user completes a task; it checks if all the tasks have been completed. If so, it calls showCongratulationAnimation().

To finish up, add the following method as the last line of tableView(_:didSelectRowAt:):

showCongratulationsIfNeeded()

Build and run.

Create a couple of tasks.

Tap on all the tasks to mark them as completed.

You should see the balloon animation!

Stopping a Timer

If you’ve glanced at the Console, you may have noticed that, even if the user has marked all the tasks as completed, the timer still continues to fire. It’s better to stop the timer for completed tasks to reduce battery drain.

First, create a new method for canceling the timer by adding the following code inside the // MARK: - Timer extension:

func cancelTimer() {
  timer?.invalidate()
  timer = nil
}

This will invalidate the timer. And, it will set it to nil so you can correctly reinitialize it again later. invalidate() is the only way to remove a Timer from a RunLoop. The RunLoop removes its strong reference to the timer either before invalidate() returns or at a later point.

Next, replace showCongratulationsIfNeeded() with the following code:

func showCongratulationsIfNeeded() {
  if taskList.filter({ !$0.completed }).count == 0 {
    cancelTimer()
    showCongratulationAnimation()
  } else {
    createTimer()
  }
}

Now, if the user completes all tasks, the app will first invalidate the timer and then show the animation; otherwise, you will try to create a new timer if one isn’t already running. This will avoid a bug when the user completes all the tasks and then creates a new one.

Build and run!

Now, the timer stops and restarts as desired.

Using CADisplayLink for Smoother Animations

Timer may not be the ideal solution for animations. You may already have noticed some frame drops during the animation — especially if you are running the app on the Simulator.

You previously set the timer at 60Hz (1 / 60). Hence, your timer will call your animation every 16ms. Take a look at the time line below:

By using Timer, you can’t be sure of the exact time an action will be triggered. It may be at the start or the end of the frames. To keep things simple, say you set the timer at the middle of each frame (the blue dots). Because it’s hard to know where the exact time of the timer, you can only be sure that you’ll get the callback every 16ms.

You now have 8ms to do your animation; this may or may not be enough time for your animation frames. Look at the second frame from the time line above. The second frame can’t be executed in time for the frame rendering. Consequently, your app will drop the second frame. You are also currently using only 8ms instead of the available 16ms.