Daily Countdown Timer in SwiftUI

Jake Sims
5 min readOct 4, 2023

--

When developing a timer, the acceptance criteria could require showing hour, minute, and seconds. With SwiftUI, this criteria can be fulfilled with a text view using the timer date style which sets up the view to display the time in hours, minutes and seconds. But what if you need to display days as well? Well that’s where I can help you out my friend. In this post, we’ll look how to build a countdown timer component that displays time in dd:hh:mm:ss format.

Prerequisite

To build, you should at least have Xcode 14.3.1 as of this writing.

Create a new SwiftUI project

Let’s create a new project, calling it whatever you desire. I named mine CountdownTimer.

Next let’s add the main view.

Create a new view

From File -> New -> File, add a new SwiftUI view, naming it whatever you want. I like to name my views with intent name along with view so I landed on CountdownTimerView but name it whatever you desire.

Next let’s setup the view to display. Now you can easily set up the view to display the timer with the intended units using a string in a Text view like so:

Text(String(format:"%02i:%02i:%02i:%02i", days, hours, minutes, seconds))

But we want to pretty it up and add a little pizazz (and not because I had already written this up. Definitely not!) Let’s build the UI like so:

struct CountdownTimerView: View {
@StateObject var viewModel: CountdownTimerViewModel

init(endDate: String) {
_viewModel = StateObject(wrappedValue: CountdownTimerViewModel(endDate: endDate))
}

var body: some View {
VStack(spacing: 16) {
HStack {
VStack(spacing: 8) {
Text(String(format: "%02d", viewModel.day))
.font(.system(size: 22, weight: .bold))
.foregroundColor(.red)
Text("day")
.textCase(.uppercase)
.font(.system(size: 11))
}
VStack(spacing: 8) {
colon
Spacer()
.frame(height: 15)
}
VStack(spacing: 8) {
Text(String(format: "%02d", viewModel.hour))
.font(.system(size: 22, weight: .bold))
.foregroundColor(.red)
Text("hour")
.textCase(.uppercase)
.font(.system(size: 11))
}
VStack(spacing: 8) {
colon
Spacer()
.frame(height: 15)
}
VStack(spacing: 8) {
Text(String(format: "%02d", viewModel.minute))
.font(.system(size: 22, weight: .bold))
.foregroundColor(.red)
Text("min")
.textCase(.uppercase)
.font(.system(size: 11))
}
VStack(spacing: 8) {
colon
Spacer()
.frame(height: 15)
}
VStack(spacing: 8) {
Text(String(format: "%02d", viewModel.second))
.font(.system(size: 22, weight: .bold))
.foregroundColor(.red)
Text("sec")
.textCase(.uppercase)
.font(.system(size: 11))
}
}
}
}
}

extension CountdownTimerView {
private var colon: some View {
Text(":")
.font(.system(size: 22, weight: .bold))
.foregroundColor(.red)
}
}

You’ll notice the view contains a view model that is used throughout the view. Let’s talk about it.

Create a view model

The most common software design pattern used in SwiftUI today is Model-View-ViewModel(MVVM). I won’t get into the details of what MVVM is right now(that’s for the next post), but we will use this pattern to abstract business logic out of the view and use view model to update it since it’s such a small app.

The view model will retain all the business logic and values that, when updated, will notify the view of a change in value and the view will update accordingly.

Note: The view model takes in a date string the conforms to a certain format. In this example, we are going with the ISO 8601 format but you can apply any format that can be applied to the date formatter. You can see all the options here.

Now let’s return to the view to make some more additions and wrap our component.

Add a timer

For the component to constantly update as the time counts down or up, we need to implement some sort of driver that will initiate the change accordingly such that it can be seen in the view. That’s where a timer comes in.

Apple provides a class called Timer that “fires after a certain time interval has elapsed, sending a specified message to a target object”. This means once a time interval threshold is met, it notifies the intended target a change has occurred. We will implement like so:

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

An instance of Timer takes various parameters to setup how the timer should be composed for it to initiate action. Here we apply the following values:

  • every interval: The time interval to measure against with interval being number of seconds.
  • on run loop: The loop on which the timer runs.
  • in which mode: The loop mode on which to run the timer.

autoConnect() enables the timer instance to automate “the process of connecting or disconnecting from this connectable publisher”.

Now that we have our timer set up, we need to enable the view to update as the timer value changes accordingly. To do this we will apply the onRecieve(_:) modifier to our view like so:

.onReceive(timer) { _ in
if hasCountdownCompleted {
timer.upstream.connect().cancel() // turn off timer
} else {
viewModel.updateTimer()
}
}

Here, our onReceive modifier takes in our timer and monitors it for any change. Once a change occurs we then check to see if our countdown has completed. If true, then we tell the timer to stop updating. Otherwise, we have our view model update the timer accordingly.

Once in place, our full view should look like:

Now, once you update your ContentView to display your CountdownTimerView, we get the following:

Conclusion

And there you have it. You have successfully created a daily countdown timer that takes in a date string and displays the time remaining. While there are more ways than one to create such a component, this is at least a good starting point. You can evolve it to count up, add a progress circle, take in different date formats, etc. I hope you found this post useful.

- 3<

--

--

Jake Sims

iOS Developer with 6 years of experience by day. Amateur Spider-man by night.