Categories
Uncategorized

Creating a Simple Stopwatch in SwiftUI

Welcome to a new SwiftUI tutorial! Today we will take a look at @ObservableObjects and @Published property wrappers, learn what they are used for and how to use them to create a simple SwiftUI stopwatch app. Understanding these property wrappers is especially important for creating functional models for your SwiftUI apps that you can use to communicate with your views.

This is what our app will look like at the end of the tutorial:



First, create a new Xcode project, select Single View App and make sure that you select SwiftUI as the “User Interface”.

Preparing the UI 🎨

We start by creating the interface for our stopwatch app. For this, we can use the default ContentView.swift file. First, replace the string of the “Hello, World” text with a placeholder for the elapsed seconds. We also use a custom font and make sure there is enough space around the text by using appropriate .paddings.

struct ContentView: View {
    var body: some View {
        Text("0.0")
            .font(.custom("Avenir", size: 40))
            . padding(.top, 200)
            .padding(.bottom, 100)
    }
}


Below the TextView, we will use Buttons to start, pause and stop the stopwatch. Wrap the TextView into a VStack and add a corresponding Button. At the moment, we just use a dummy print statement as placeholder. 

VStack {
            Text("0.0")
                .font(.custom("Avenir", size: 40))
                .padding(.top, 200)
                .padding(.bottom, 100)
            Button(action: {print("Start timer.")}) {
                Text("Start")
                    .foregroundColor(.white)
                    .padding(.vertical, 20)
                    .padding(.horizontal, 90)
                    .background(Color.blue)
                    .cornerRadius(10)
            }
        }


Later, we will use this button design for the pause and stop buttons as well, each with different text and color. To make it reusable, CMD-click on the Text view of our button and select “Extract Subview”. We can call the extracted subview TimerButton.

As already mentioned, the text and the color of the button should be changeable. Therefore, we use dynamic properties in our TimerButton struct instead of fixed values, which we then initialize from our ContentView. So change the TimerButton struct as follows:

struct TimerButton: View {
    
    let label: String
    let buttonColor: Color
    
    var body: some View {
        Text(label)
            .foregroundColor(.white)
            .padding(.vertical, 20)
            .padding(.horizontal, 90)
            .background(buttonColor)
            .cornerRadius(10)
    }
}


Now, we have to initialize the parameters in our ContentView accordingly.

VStack {
            //...
            Button(action: {print("Start Timer")}) {
                TimerButton(label: "Start", buttonColor: .blue)
            }
        }


Finally, we push the views inside our VStack upwards by inserting a Spacer.

VStack {
            Text("0.0")
                //...
           Button(action: {print("Start Timer")}) {
                TimerButton(label: "Start", buttonColor: .blue)
            }
            Spacer()
        }


So much for the preparations for the UI of our stopwatch app. Your SwiftUI preview for your stopwatch app should now look like this:

Setting up our StopWatchManager ⏱

For our SwiftUI stopwatch app, we need an instance that notifies our ContentView every time unit (e.g. every tenth of a second) after we start the timer and adjust the TextView accordingly.

This functionality is ideally placed in a separate class. Therefore, we create a new Swift file, which we call StopWatchManager.swift In this file, we import the SwiftUI framework and create a new class, which we also call StopWatchManager. 

import SwiftUI

class StopWatchManager {
    
}


Our StopWatchManager needs a variable with which we keep track of how much time has passed since the timer was started. This variable will be called secondsElapsed. We also initialize a Swift Timer instance

class StopWatchManager {
    
    var secondsElapsed = 0.0
    var timer = Timer()
}


We also create a function that starts the timer, which adds the value 0.1 to our secondsElapsed every 0.1 seconds. 

class StopWatchManager {
    
    //...
    
    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            self.secondsElapsed += 0.1
        }
    }
}


We can now initialize the StopWatchManager in our ContentView and access the respective value of the secondsElapsed property for the TextView. We also want to trigger the start function when we tap the corresponding Button.

struct ContentView: View {
    
    var stopWatchManager = StopWatchManager()
    
    var body: some View {
        VStack {
            Text(String(format: "%.1f", stopWatchManager.secondsElapsed))
                //...
            Button(action: {self.stopWatchManager.start()}) {
                TimerButton(label: "Start", buttonColor: .blue)
            }
            //...
        }
    }
}


Now, launch the app in the live preview and tap the start Button. You will see that the preview does not change. This is because although the timer in our StopWatchManager starts to add 0.1 to the secondsElapsed property every tenth of a second, our ContentView is not being notified about these changes and therefore does not show the continuously updated values. 

Using the @ObservableObject and @Published property wrapper functionality 🛠

However, we can easily implement this functionality. To do this, we use the so-called ObservableObject protocol.

class StopWatchManager: ObservableObject {
    
    //...
}


ObservableObjects are similar to State properties which you probably already know. But instead of just (re)rendering a view depending on its assigned data, ObservableObjects are capable of the following things:

  • We can bind one or multiple views to the ObservableObject (or better said, we can make these views observe the object).
  • The observing views can access and manipulate the data inside the ObservableObject. When a change happens to the ObservableObject’s data all observing views get automatically rerendered, similar to when a State changes

We want our ContentView to observe our StopWatchManager to “listen” to changes in it. To do this, we use the @ObservedObject property wrapper in front of the initialisation of the StopWatchManager property.

struct ContentView: View {
    
    @ObservedObject var stopWatchManager = StopWatchManager()
    
    var body: some View {
        //...
    }
}


Okay, our ContentView is now able to listen and to react to changes in the stopWatchManager instance. But how do we specifically notify and tell the ContentView to rerender every time the value assigned to the secondsElapsed property changes? Well, that’s quite easy: Just place the @Published property wrapper in front of it!

class StopWatchManager: ObservableObject {
    
    @Published var secondsElapsed = 0.0
    
    //...
}


This property wrapper tells all observing views (including our ContentView!) to reload themselves whenever the value assigned to secondsElapsed changes.

Let’s rerun the preview of our ContentView in live mode and check if that works. 



Awesome! After you tap the “Start” button, the timer in our stopWatchManager every 0.1 seconds adds 0.1 to our secondsElapsed variable. And since its a @Published variable and our ContentView observes the StopWatchManager ObservableObject, our ContentView will rerender every 0.1 seconds as well with always showing exactly how many seconds have already elapsed!

Hint: If it doesn’t work in your ContentView preview, try to run your app in the “regular” simulator.

Completing the timer functionality ⏲

Okay, we now know how @ObservableObject and @Published property wrappers work and have used this knowledge to implement our basic timer in SwiftUI. Let’s finish our SwiftUI stopwatch app by implementing the possibility to pause and stop the timer. 

First, we add a corresponding enum under our StopWatchManager class, so that later on we always know if our stopwatch is currently running, paused or stopped.

enum stopWatchMode {
    case running
    case stopped
    case paused
}


Next, we add another @Published property to our StopWatchManager so that every time our timer is started, paused or stopped, our ContentView can react and adapt accordingly.

class StopWatchManager: ObservableObject {
    
    @Published var mode: stopWatchMode = .stopped
    //...
}


When our timer starts, we set the mode to .running.

func start() {
        mode = .running
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            self.secondsElapsed = self.secondsElapsed + 0.1
        }
    }


Next, we add two more functions that pause and reset the timer and change the mode accordingly.

class StopWatchManager: ObservableObject {
    
    //...
    
    func stop() {
        timer.invalidate()
        secondsElapsed = 0
        mode = .stopped
    }
    
}


Finishing our UI behaviour 👨‍💻

Great, our StopWatchManager can now also pause and stop the timer and furthermore our ContentView always knows whether the timer is running, paused or stopped through the @Published mode property.

Depending on the mode, we want to show different buttons in our ContentView. So if the timer mode is currently .stopped we want to show the start button.

VStack {
            Text(String(format: "%.1f", stopWatchManager.secondsElapsed))
                //...
            if stopWatchManager.mode == .stopped {
                Button(action: {self.stopWatchManager.start()}) {
                    TimerButton(label: "Start", buttonColor: .blue)
                }
            }
            Spacer()
        }


On the other hand, if the timer is running, we want to display a button that pauses the timer, so we write:


VStack {
            Text(String(format: "%.1f", stopWatchManager.secondsElapsed))
                //...
            if stopWatchManager.mode == .stopped {
                //...
            }
            if stopWatchManager.mode == .running {
                Button(action: {self.stopWatchManager.pause()}) {
                    TimerButton(label: "Pause", buttonColor: .blue)
                }
            }
            Spacer()
        }


And when the timer is eventually paused, we want to show two buttons: one to resume the timer and one to stop it.

VStack {
            Text(String(format: "%.1f", stopWatchManager.secondsElapsed))
                //...
            if stopWatchManager.mode == .stopped {
                //...
            }
            if stopWatchManager.mode == .running {
                //...
            }
            if stopWatchManager.mode == .paused {
                Button(action: {self.stopWatchManager.start()}) {
                    TimerButton(label: "Start", buttonColor: .blue)
                }
                Button(action: {self.stopWatchManager.stop()}) {
                    TimerButton(label: "Stop", buttonColor: .red)
                }
                    .padding(.top, 30)
            }
            Spacer()
        }


If we now run our app, we can start, pause and stop our stopwatch!

Conclusion 🎊

That’s it! We learned how to communicate between models and views in SwiftUI by using @ObservableObjects and @Published modifiers and used that knowledge to create a simple stopwatch app in SwiftUI.

You can find the source code for this app here.

We hope you liked this tutorial. Let us know what you think about it in the comments 👇 If you want to learn more about SwiftUI, make sure you check out our free SwiftUI Basics eBook and our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!