It becomes quite often necessary when implementing apps to create tasks that are either being repeated at a constant interval, or they are executed after a certain period of time. For instance, we might want to display a message for a few seconds and then make it disappear, or to save user-edited text periodically. In order to serve such purposes, the Foundation framework provides us with a handy class to use, the well known Timer.
Quite probably, you are already familiar with Timer, one way or another. In SwiftUI, we mostly use its more suitable Combine-related APIs, while in UIKit or AppKit we usually schedule an instance of it using specific static methods. Admittedly, it’s not difficult to work with Timer when coding. But even the few steps of preparation might feel sometimes as an extra hassle that we’d like to avoid, especially when there’s a pile of other tasks to implement or fix. So, in this post I’ll show how to create a simple wrapper that will make it easier to start and stop a timer in Swift.
The custom timer type
We’ll start the implementation of the timer wrapper by defining a new custom type; a class in particular, which we’ll name HandyTimer
-????-:
1 2 3 4 5 |
class HandyTimer { } |
We’ll declare two stored properties initially in this class. The first one will be a Timer object which we’ll keep private; we don’t want others to temper with it. The second property is going to be a closure that will be called every time the timer fires:
1 2 3 4 |
private var timer: Timer? var onFire: (() -> Void)? |
Besides these two, we’ll also declare a read-only computed property that will be indicating whether the timer is running or not:
1 2 3 4 5 |
var isRunning: Bool { timer != nil && timer!.isValid } |
The above does not only check if the timer is other than nil, but it also makes sure that it’s valid as well.
It’s now time to implement the first method of the class. That’s going to be the one that will be invoked whenever the timer fires. Note that we must annotate it with the @objc
keyword, as that’s a requirement coming from the way we’ll initialize and start the timer next:
1 2 3 4 5 6 |
@objc fileprivate func handleTimerEvent() { onFire?() } |
The only thing we have to do in handleTimerEvent()
method is to call the action closure that we declared previously. So far we have not assigned a value to it, but that’s something that we’re fixing right next.
The method that follows in the next snippet, is the one that initializes and fires the timer
object. It contains three parameter values:
- The time interval that specifies the firing frequency of the timer.
- A flag indicating whether the timer will keep repeating firing, or it will get invalidated right after the first time it will fire. We’ll set a default value to this one in order to make things even easier if repeating is a desired behavior in the timer.
- A closure to handle the fire event.
Once we ensure that the timer is not already running, we’ll initialize it using a specific static method that you see next. Additionally, we’ll assign the closure given as argument to the onFire
property, so it has a value when it will be called in the handleTimerEvent()
method.
1 2 3 4 5 6 7 8 9 10 11 12 |
func start(withTimeInterval timeInterval: TimeInterval, repeats: Bool = true, onFire: @escaping () -> Void) { guard !isRunning else { return } timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(handleTimerEvent), userInfo: nil, repeats: repeats) self.onFire = onFire } |
The handleTimerEvent()
is provided as argument to the selector of the scheduledTimer(timeInterval:target:selector:userInfo:repeats:)
static method that initializes and puts the timer in motion. Also notice that the repeats
parameter value is given as argument to the last parameter of that method.
When the given time interval has elapsed, the timer will fire and the handleTimerEvent()
method will be invoked. Unless we pass false
for the repeat
argument, this will keep happening until we manually invalidate and stop the timer.
However, so far we have not implemented a way to do that, so let’s add the next method to the HandyTimer
class:
1 2 3 4 5 6 7 |
func stop() { timer?.invalidate() timer = nil onFire = nil } |
See that not only we invalidate the timer here, but also set nil
to both stored properties, timer
and onFire
.
The HandyTimer
wrapper is now ready, so let’s see it as a single piece of code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class HandyTimer { private var timer: Timer? var onFire: (() -> Void)? var isRunning: Bool { timer != nil && timer!.isValid } @objc fileprivate func handleTimerEvent() { onFire?() } func start(withTimeInterval timeInterval: TimeInterval, repeats: Bool = true, onFire: @escaping () -> Void) { guard !isRunning else { return } timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(handleTimerEvent), userInfo: nil, repeats: repeats) self.onFire = onFire } func stop() { timer?.invalidate() timer = nil onFire = nil } } |
Using the timer wrapper
We are going to see HandyTimer
in action using a quite simple SwiftUI based example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
struct ContentView: View { @State private var shouldShowMessage = false @State private var message = “” var timer = HandyTimer() var body: some View { ZStack { VStack { Spacer() Button { showMessageView() } label: { Text(“Show Random Number”) .font(.title2) } .padding(.bottom, 40) } if shouldShowMessage { MessageView(message: $message) } } } } |
The above implementation displays a button at the bottom of the screen. When tapped, it triggers the appearance of another view (MessageView
), which will be simply showing a random number as its message. That view will be dismissed after a period of time, and for that we’ll use the timer wrapper implemented earlier.
All that will take place in the showMessageView()
method invoked in the button’s action closure above. Before going to that, however, let me share with you the MessageView
implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct MessageView: View { @Binding var message: String var body: some View { ZStack { Color(.black).opacity(0.5) Text(message) .font(.title) .fontWeight(.medium) .foregroundColor(.white.opacity(0.9)) .padding() } .fixedSize(horizontal: true, vertical: false) .cornerRadius(8) .frame(height: 60) } } |
The binding value of the message that should be displayed is given as argument upon the initialization of this view. This also becomes obvious from the previous snippet, where the MessageView
is presented conditionally:
1 2 3 4 5 |
if shouldShowMessage { MessageView(message: $message) } |
Focusing on the showMessageView()
method now that is called when the button is tapped, let’s get rid of the initial tasks; let’s prepare the message with a random number, and let’s change the shouldShowMessage
flag value using a simple animation when the message view is about to appear:
1 2 3 4 5 6 7 8 9 10 11 |
func showMessageView() { message = “Random number: \(Int.random(in: 100…1000))” if !shouldShowMessage { withAnimation(.linear(duration: 0.15)) { shouldShowMessage.toggle() } } } |
One of the properties declared in the ContentView
that I demonstrated at the beginning of this part is the handyTimer
, which is initialized at the same time we declare it. Our next move in the showMessageView()
is to use it, and start the timer so the message view to be dismissed after a predefined amount of time:
1 2 3 4 5 6 7 8 9 10 11 12 |
func showMessageView() { … timer.start(withTimeInterval: 0.6) { withAnimation(.linear(duration: 0.15)) { shouldShowMessage.toggle() } timer.stop() } } |
See how easy it becomes to use a timer with this custom wrapper. Besides its declaration at the beginning, the above is all we need in order to start the timer and handle the fire event in the closure we provide as argument.
In this particular case, the time interval is set to 0.6 seconds; the message view will be displayed for that amount of time. On the fire event of the timer, we toggle the shouldShowMessage
state property in an animation block once again, and then we stop the timer.
Just because we don’t let the timer repeat and we stop it instantly, we could remove the timer.stop()
line, and provide the repeats
argument with the false
value as shown next:
1 2 3 4 5 6 7 8 9 10 11 |
func showMessageView() { … timer.start(withTimeInterval: 0.6, repeats: false) { withAnimation(.linear(duration: 0.15)) { shouldShowMessage.toggle() } } } |
However, what should happen on subsequent button taps that would lead to new showMessageView()
calls? Obviously, the displayed message will keep changing, as it’s the binding value of the message
state property that we provide to message view. But for how long is it going to be displayed, given that the timer is already ticking?
To manage displaying all messages for the same amount of time, the timer should be stopped and restarted every time the showMessageView()
method gets called. Thankfully, our wrapper helps us do that pretty easily, and just by appending the following right before we start the timer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func showMessageView() { … // Stop the timer if it’s already running. if timer.isRunning { timer.stop() } timer.start(withTimeInterval: 0.6, repeats: false) { … } } |
With that addition, each message will be shown for 0.6 seconds, no matter if the button was tapped while another message has been shown already.
Actually, the above is quite common, so we could embed it as functionality to the method that starts the timer. This is going to take just a couple of small additions.
So, back to the HandyTimer
and in the start(withTimeInterval:repeats:onFire:)
implementation. We’ll add another parameter here, right after repeats
and before onFire
. We’ll call it restart
, it’s going to be a boolean value, and by default we’ll set its value to false:
1 2 3 4 5 6 7 8 |
func start(withTimeInterval timeInterval: TimeInterval, repeats: Bool = true, restart: Bool = false, onFire: @escaping () -> Void) { … } |
The restart
flag will be defining one simple thing; when true, it will be stopping the timer if it’s already running. To achieve that, we only have to add the following lines at the beginning of the method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func start(withTimeInterval timeInterval: TimeInterval, repeats: Bool = true, restart: Bool = false, onFire: @escaping () -> Void) { // Call stop() and stop the timer if it’s already running. if restart && isRunning { stop() } … } |
Back to the SwiftUI view and in the showMessageView()
again, where we can now initialize the timer and provide true
as the value for the restart
argument:
1 2 3 4 5 6 7 8 9 10 |
func showMessageView() { … // Start the timer passing true to the restart argument. timer.start(withTimeInterval: 0.6, repeats: false, restart: true) { … } } |
Overview
Working with the Timer API is not difficult, and with the wrapper class we implemented in this post we made using it even simpler. This task that might seem unnecessary at first, can eventually help us write a bit less, and more clear code. Crafting simple solutions like the one demonstrated here add value to the overall coding process, as they can lift unneeded friction in the workflow, no matter if the original tools have no particular difficulty when using them. I hope you found this topic interesting and helpful for your own projects.
Thank you for reading, take care! ????