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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS Timer Tutorial
20 mins
- Getting Started
- Creating Your First Timer
- Adding Timer Tolerance
- Trying Out Timers in the Background
- Understanding Run Loops
- Utilizing Run Loop Modes
- Adding a Task Completion Animation
- Showing the Animation
- Stopping a Timer
- Using CADisplayLink for Smoother Animations
- CADisplayLink to the Rescue!
- Where to Go From Here?
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:
- default: Handles input sources that are not NSConnectionObjects.
- common: Handles a set of run loop modes for which you can define a set of sources, timers and observers.
- 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:
- Handle the animation timer.
- Take care of the animation start time and end time.
- Specify the animation duration.
- 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:
- Calculate the right height for the animation based on the device’s screen height.
- Center the balloon outside of the screen and set its visibility.
- Create the
startTime
and calculate theendTime
by adding theanimationDuration
to thestartTime
. - 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:
- Check that the
endTime
andstartTime
are notnil
. - Save the current time to a constant.
- Ensure that the current time has not passed the end time. And in case it has, invalidate the timer and hide the balloon.
- Calculate the animation percentage and the desired y-coordinate the balloon should be moved to.
- 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.