
IOS Timer
- Transfer
- Tutorial
Imagine that you are working on an application in which you need to periodically perform some actions. This is exactly what Swift uses the Timer class for .
Timer is used to plan actions in the application. This can be a one-time action or a repeating procedure.
In this guide, you will learn how the timer works in iOS, how it can affect the responsiveness of the UI, how to optimize battery consumption when using a timer, and how to use CADisplayLink for animation.
As a test site, we will use the application - a primitive task scheduler.
Download the source project. Open it in Xcode, look at its structure, compile and execute. You will see the simplest task scheduler:

Add a new task to it. Tap on the + icon, enter the name of the task, tap Ok.
Added tasks have a timestamp. The new task you just created is marked with zero seconds. As you can see, this value does not increase.
Each task can be marked as completed. Tap on the task. The task name will be crossed out and it will be marked as completed.
Let's create the main timer of our application. The Timer class , also known as NSTimer, is a convenient way to schedule an action for a specific moment, both single and periodic.
Open TaskListViewController.swift and add this variable to TaskListViewController :
Then add the extension there:
And paste this code into the extension:
In this method, we:
Then add this code to the extension:
Here we:
Then we need to create a timer as soon as the user adds the first task. Add createTimer () at the very beginning of the presentAlertController (_ :) method .
Launch the application and create a couple of new tasks. You will see that the time stamp for each task changes every second.

Increasing the number of timers leads to worse UI responsiveness and more battery consumption. Each timer is trying to be fulfilled precisely in his allotted time, as its default tolerance ( tolerance ) is zero.
Adding a timer tolerance is an easy way to reduce energy consumption. This allows the system to perform a timer action between the assigned time and the assigned time plus the tolerance time - but never before the assigned interval.
For timers that run only once, the tolerance value is ignored.
In the createTimer () method , immediately after the timer assignment, add this line:
Launch the app. In this particular case, the effect will not be obvious (we only have one timer), however, in the real situation of several timers, your users will get a more responsive interface and the application will be more energy efficient.

Interestingly, what happens to timers when an application goes into the background? To deal with this, add this code at the very beginning of the updateTimer () method :
This will allow us to track timer events in the console.
Launch the application, add the task. Now press the Home button on your device, and then return to our application.
In the console, you will see something like this:

As you can see, when the application goes to the background, iOS pauses all running application timers. When the application becomes active, iOS resumes the timers.
A run loop is an event loop that schedules work and handles incoming events. The cycle keeps the thread busy while it is running and puts it in a "sleeping" state when there is no work for it.
Each time you start the application, the system creates the main thread of the application, each thread has an automatically created execution loop for it.
But why is all this information important to you now? Now every timer starts in the main thread and joins the execution loop. You are probably aware that the main thread is engaged in rendering the user interface, handling touches, and so on. If the main thread is busy with something, the interface of your application may become "unresponsive" (hang).
Did you notice that the timestamp in the cell is not updated when you drag the table view?

You can solve this problem by telling the run cycle to start timers in a different mode.
A run cycle mode is a set of input sources, such as touching a screen or clicking with a mouse, that can be monitored and a set of “observers” that receive notifications.
There are three runtime modes in iOS:
default : input sources that are not NSConnectionObjects are processed.
common : a set of input cycles is being processed, for which you can define a set of input sources, timers, "observers".
tracking : the application UI is being processed.
For our application, the most suitable mode is common . To use it, replace the contents of the createTimer () method with the following:
The main difference from the previous code is that before assigning a timer to TaskListViewController, we add this timer to the run loop in common mode .
Compile and run the application.

Now the timestamps of the cells are updated even if the table is scrolled.
Now we add a congratulatory animation for the user to complete all tasks - the ball will rise from the bottom of the screen to the very top.
Add these variables at the beginning of the TaskListViewController:
The purpose of these variables is:
Now add the following TaskListViewController extension at the end of the TaskListViewController.swift file :
Here we do the following:
Now we need to create the actual logic for updating the congratulatory animation. Add this code after showCongratulationAnimation () :
What are we doing:
Now replace // TODO: Animation here in showCongratulationAnimation () with this code:
Now updateAnimation () is called whenever a timer event occurs.
Hooray, we just created an animation. However, when the application starts, nothing new happens ...
As you probably guessed, there is nothing to “launch” our new animation. To do this, we need another method. Add this code to the TaskListViewController animation extension :
We will call this method whenever the user marks the task completed, he checks whether all tasks are completed. If so, it will call showCongratulationAnimation () .
In conclusion, add a call to this method at the end of tableView (_: didSelectRowAt :) :
Launch the application, create a couple of tasks, mark them as completed - and you will see our animation!

If you look at the console, you will see that although the user marked all the tasks completed, the timer continues to work. This is completely pointless, so it makes sense to stop the timer when it is not needed.
First, create a new method to stop the timer:
This will update the timer and reset it to nil so that we can correctly create it again later. invalidate () is the only way to remove a Timer from a run loop. The run loop will remove the strong timer reference either immediately after calling invalidate () or a little later.
Now replace the showCongratulationsIfNeeded () method as follows:
Now, if the user completes all the tasks, the application will first reset the timer and then show the animation, otherwise it will try to create a new timer if it is not already there.
Launch the app.

Now the timer stops and restarts as it should.
Timer is not an ideal choice for controlling animation. You might notice a few frames skipping animation, especially if you run the application in the simulator.
We set the timer to 60Hz. Thus, the timer updates the animation every 16 ms. Consider the situation more closely:

When using Timer, we do not know the exact time the action started. This can happen either at the beginning or at the end of the frame. Let's say the timer runs in the middle of each frame (blue dots in the picture). The only thing we know for sure is that the call will be every 16 ms.
Now we only have 8 ms to execute the animation, and this may not be enough for our animation. Let's look at the second frame in the figure. The second frame cannot be completed in the allotted time, so the application will reset the second frame of the animation.
CADisplayLink is called once per frame and tries to synchronize real animation frames as much as possible. Now you will have all 16 ms at your disposal and iOS will not drop a single frame.
To use CADisplayLink , you need to replace animationTimer with a new type.
Replace this code
on this:
You have replaced Timer with CADisplayLink . CADisplayLink is a timer view that is tied to the vertical scan of the display. This means that the device’s GPU will pause until the screen can continue to process GPU commands. This way we get smooth animation.
Replace this code
on this:
You replaced TimeInterval with CFTimeInterval , which is necessary for working with CADisplayLink.
Replace the text of the showCongratulationAnimation () method with this:
What are we doing here:
Now replace updateAnimation () with the following code:
Launch the app!

Perfectly! We successfully replaced the Timer- based animation with a more appropriate CADisplayLink - and got a smoother animation without jerking.
In this guide, you figured out how the Timer class works in iOS, what the execution cycle is and how it can make your application more responsive in terms of interface, and how to use CADisplayLink instead of Timer for smooth animation.
Timer is used to plan actions in the application. This can be a one-time action or a repeating procedure.
In this guide, you will learn how the timer works in iOS, how it can affect the responsiveness of the UI, how to optimize battery consumption when using a timer, and how to use CADisplayLink for animation.
As a test site, we will use the application - a primitive task scheduler.
Getting started
Download the source project. Open it in Xcode, look at its structure, compile and execute. You will see the simplest task scheduler:

Add a new task to it. Tap on the + icon, enter the name of the task, tap Ok.
Added tasks have a timestamp. The new task you just created is marked with zero seconds. As you can see, this value does not increase.
Each task can be marked as completed. Tap on the task. The task name will be crossed out and it will be marked as completed.
Create our first timer
Let's create the main timer of our application. The Timer class , also known as NSTimer, is a convenient way to schedule an action for a specific moment, both single and periodic.
Open TaskListViewController.swift and add this variable to TaskListViewController :
var timer: Timer?
Then add the extension there:
// MARK: - Timer
extension TaskListViewController {
}
And paste this code into the 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()
}
}
}
In this method, we:
- Check if there are visible rows in the task table.
- We call updateTime for each visible cell. This method updates the timestamp in the cell (see TaskTableViewCell.swift ).
Then add this code to the extension:
func createTimer() {
// 1
if timer == nil {
// 2
timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(updateTimer),
userInfo: nil,
repeats: true)
}
}
Here we:
- Check if the timer contains an instance of the Timer class .
- If not, create a timer that calls updateTimer () every second .
Then we need to create a timer as soon as the user adds the first task. Add createTimer () at the very beginning of the presentAlertController (_ :) method .
Launch the application and create a couple of new tasks. You will see that the time stamp for each task changes every second.

Add timer tolerance
Increasing the number of timers leads to worse UI responsiveness and more battery consumption. Each timer is trying to be fulfilled precisely in his allotted time, as its default tolerance ( tolerance ) is zero.
Adding a timer tolerance is an easy way to reduce energy consumption. This allows the system to perform a timer action between the assigned time and the assigned time plus the tolerance time - but never before the assigned interval.
For timers that run only once, the tolerance value is ignored.
In the createTimer () method , immediately after the timer assignment, add this line:
timer?.tolerance = 0.1
Launch the app. In this particular case, the effect will not be obvious (we only have one timer), however, in the real situation of several timers, your users will get a more responsive interface and the application will be more energy efficient.

Timers in the background
Interestingly, what happens to timers when an application goes into the background? To deal with this, add this code at the very beginning of the updateTimer () method :
if let fireDateDescription = timer?.fireDate.description {
print(fireDateDescription)
}
This will allow us to track timer events in the console.
Launch the application, add the task. Now press the Home button on your device, and then return to our application.
In the console, you will see something like this:

As you can see, when the application goes to the background, iOS pauses all running application timers. When the application becomes active, iOS resumes the timers.
Investigated with the execution cycles ( Run Loops )
A run loop is an event loop that schedules work and handles incoming events. The cycle keeps the thread busy while it is running and puts it in a "sleeping" state when there is no work for it.
Each time you start the application, the system creates the main thread of the application, each thread has an automatically created execution loop for it.
But why is all this information important to you now? Now every timer starts in the main thread and joins the execution loop. You are probably aware that the main thread is engaged in rendering the user interface, handling touches, and so on. If the main thread is busy with something, the interface of your application may become "unresponsive" (hang).
Did you notice that the timestamp in the cell is not updated when you drag the table view?

You can solve this problem by telling the run cycle to start timers in a different mode.
Understanding Run Cycle Modes
A run cycle mode is a set of input sources, such as touching a screen or clicking with a mouse, that can be monitored and a set of “observers” that receive notifications.
There are three runtime modes in iOS:
default : input sources that are not NSConnectionObjects are processed.
common : a set of input cycles is being processed, for which you can define a set of input sources, timers, "observers".
tracking : the application UI is being processed.
For our application, the most suitable mode is common . To use it, replace the contents of the createTimer () method with the following:
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 from the previous code is that before assigning a timer to TaskListViewController, we add this timer to the run loop in common mode .
Compile and run the application.

Now the timestamps of the cells are updated even if the table is scrolled.
Add animation to complete all tasks
Now we add a congratulatory animation for the user to complete all tasks - the ball will rise from the bottom of the screen to the very top.
Add these variables at the beginning of the TaskListViewController:
// 1
var animationTimer: Timer?
// 2
var startTime: TimeInterval?, endTime: TimeInterval?
// 3
let animationDuration = 3.0
// 4
var height: CGFloat = 0
The purpose of these variables is:
- animation timer storage.
- storage of time of the beginning and the end of animation.
- animation duration.
- animation height.
Now add the following TaskListViewController extension at the end of the TaskListViewController.swift file :
// 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
}
}
}
Here we do the following:
- calculate the height of the animation, getting the height of the device screen
- center the ball outside the screen and set its visibility
- assign the start and end time of the animation
- we start the animation timer and update the animation 60 times per second
Now we need to create the actual logic for updating the congratulatory animation. Add this 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)
}
What are we doing:
- check that endTime and startTime are assigned
- save current time in constant
- we make sure that the final time has not yet arrived. If it has already arrived, update the timer and hide our ball
- calculate the new y-coordinate of the ball
- the horizontal position of the ball is calculated relative to the previous position
Now replace // TODO: Animation here in showCongratulationAnimation () with this code:
self.updateAnimation()
Now updateAnimation () is called whenever a timer event occurs.
Hooray, we just created an animation. However, when the application starts, nothing new happens ...
Showing animation
As you probably guessed, there is nothing to “launch” our new animation. To do this, we need another method. Add this code to the TaskListViewController animation extension :
func showCongratulationsIfNeeded() {
if taskList.filter({ !$0.completed }).count == 0 {
showCongratulationAnimation()
}
}
We will call this method whenever the user marks the task completed, he checks whether all tasks are completed. If so, it will call showCongratulationAnimation () .
In conclusion, add a call to this method at the end of tableView (_: didSelectRowAt :) :
showCongratulationsIfNeeded()
Launch the application, create a couple of tasks, mark them as completed - and you will see our animation!

We stop the timer
If you look at the console, you will see that although the user marked all the tasks completed, the timer continues to work. This is completely pointless, so it makes sense to stop the timer when it is not needed.
First, create a new method to stop the timer:
func cancelTimer() {
timer?.invalidate()
timer = nil
}
This will update the timer and reset it to nil so that we can correctly create it again later. invalidate () is the only way to remove a Timer from a run loop. The run loop will remove the strong timer reference either immediately after calling invalidate () or a little later.
Now replace the showCongratulationsIfNeeded () method as follows:
func showCongratulationsIfNeeded() {
if taskList.filter({ !$0.completed }).count == 0 {
cancelTimer()
showCongratulationAnimation()
} else {
createTimer()
}
}
Now, if the user completes all the tasks, the application will first reset the timer and then show the animation, otherwise it will try to create a new timer if it is not already there.
Launch the app.

Now the timer stops and restarts as it should.
CADisplayLink for smooth animation
Timer is not an ideal choice for controlling animation. You might notice a few frames skipping animation, especially if you run the application in the simulator.
We set the timer to 60Hz. Thus, the timer updates the animation every 16 ms. Consider the situation more closely:

When using Timer, we do not know the exact time the action started. This can happen either at the beginning or at the end of the frame. Let's say the timer runs in the middle of each frame (blue dots in the picture). The only thing we know for sure is that the call will be every 16 ms.
Now we only have 8 ms to execute the animation, and this may not be enough for our animation. Let's look at the second frame in the figure. The second frame cannot be completed in the allotted time, so the application will reset the second frame of the animation.
CADisplayLink helps us
CADisplayLink is called once per frame and tries to synchronize real animation frames as much as possible. Now you will have all 16 ms at your disposal and iOS will not drop a single frame.
To use CADisplayLink , you need to replace animationTimer with a new type.
Replace this code
var animationTimer: Timer?
on this:
var displayLink: CADisplayLink?
You have replaced Timer with CADisplayLink . CADisplayLink is a timer view that is tied to the vertical scan of the display. This means that the device’s GPU will pause until the screen can continue to process GPU commands. This way we get smooth animation.
Replace this code
var startTime: TimeInterval?, endTime: TimeInterval?
on this:
var startTime: CFTimeInterval?, endTime: CFTimeInterval?
You replaced TimeInterval with CFTimeInterval , which is necessary for working with CADisplayLink.
Replace the text of the showCongratulationAnimation () method with this:
func showCongratulationAnimation() {
// 1
height = UIScreen.main.bounds.height + balloon.frame.size.height
balloon.center = CGPoint(x: UIScreen.main.bounds.width / 2,
y: height + balloon.frame.size.height / 2)
balloon.isHidden = false
// 2
startTime = CACurrentMediaTime()
endTime = animationDuration + startTime!
// 3
displayLink = CADisplayLink(target: self,
selector: #selector(updateAnimation))
displayLink?.add(to: RunLoop.main, forMode: .common)
}
What are we doing here:
- Set the height of the animation, the coordinates of the ball and visibility - about the same as they did before.
- Initialize startTime with CACurrentMediaTime () (instead of of Date ()).
- We create an instance of the CADisplayLink class and add it to the run loop in common mode .
Now replace updateAnimation () with the following code:
// 1
@objc func updateAnimation() {
guard
let endTime = endTime,
let startTime = startTime
else {
return
}
// 2
let now = CACurrentMediaTime()
if now >= endTime {
// 3
displayLink?.isPaused = true
displayLink?.invalidate()
balloon.isHidden = true
}
let percentage = (now - startTime) * 100 / animationDuration
let y = height - ((height + balloon.frame.height / 2) / 100 *
CGFloat(percentage))
balloon.center = CGPoint(x: balloon.center.x +
CGFloat.random(in: -0.5...0.5), y: y)
}
- Add objc to the method signature (for CADisplayLink, the selector parameter requires such a signature).
- Replace initialization with Date () to initialize the date of CoreAnimation .
- Replace the animationTimer.invalidate () call with a pause of CADisplayLink and invalidate. This will also remove CADisplayLink from the run loop.
Launch the app!

Perfectly! We successfully replaced the Timer- based animation with a more appropriate CADisplayLink - and got a smoother animation without jerking.
Conclusion
In this guide, you figured out how the Timer class works in iOS, what the execution cycle is and how it can make your application more responsive in terms of interface, and how to use CADisplayLink instead of Timer for smooth animation.