Implementing A Simple Timer Wrapper In Swift

April 15th, 2022

⏱ Reading Time: 7 mins

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 -????-:

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:

Besides these two, we’ll also declare a read-only computed property that will be indicating whether the timer is running or not:

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:

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.

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:

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:

Using the timer wrapper

We are going to see HandyTimer in action using a quite simple SwiftUI based example:

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:

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:

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:

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:

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:

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:

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.

An iPhone simulator showing a button that is being tapped and displays a message view that disappears automatically after a period of time. It also shows that the message view appearing duration is refreshed on subsequent button tappings.

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:

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:

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:

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! ????

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.