SwiftUI offers a declarative way to build user interfaces across all Apple platforms. Creating a stopwatch or timer app is a fantastic project to learn the fundamentals of SwiftUI while building a practical and engaging application.
Introduction
In this comprehensive guide, we’ll walk through the process of building a stopwatch/timer app using SwiftUI. We’ll cover the core concepts, code snippets, and best practices to ensure you grasp every aspect of the development process.
Project Setup
Let’s start by setting up our project. Open Xcode and create a new iOS project. Choose the “App” template and make sure you select SwiftUI for the Interface. Name your project something appropriate like “StopwatchApp.”
Basic UI Layout
We’ll begin with a basic layout that includes a text display for the timer, start/stop button, and a reset button.
Step 1: Define UI Elements
import SwiftUI
struct ContentView: View {
@State private var timeElapsed: Double = 0.0
@State private var isRunning: Bool = false
var body: some View {
VStack {
Text(String(format: "%.2f", timeElapsed))
.font(.system(size: 50, weight: .medium, design: .monospaced))
.padding()
HStack {
Button(action: {
isRunning ? stopTimer() : startTimer()
}) {
Text(isRunning ? "Stop" : "Start")
.font(.title2)
.padding()
.background(isRunning ? Color.red : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
resetTimer()
}) {
Text("Reset")
.font(.title2)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
.padding()
}
func startTimer() {
isRunning = true
}
func stopTimer() {
isRunning = false
}
func resetTimer() {
isRunning = false
timeElapsed = 0.0
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Explanation:
- We define a
ContentView
struct that conforms to theView
protocol. @State private var timeElapsed: Double = 0.0
initializes the time elapsed and makes it a state variable to trigger UI updates.@State private var isRunning: Bool = false
indicates whether the timer is running or not.- A
VStack
is used to arrange the text display and buttons vertically. - An
HStack
is used to arrange the “Start/Stop” and “Reset” buttons horizontally. - The “Start/Stop” button toggles the timer state and changes its text and color accordingly.
- The “Reset” button resets the timer to zero.
Implementing the Timer Logic
Next, we need to implement the actual timer logic to update the time elapsed.
Step 2: Add a Timer
import SwiftUI
struct ContentView: View {
@State private var timeElapsed: Double = 0.0
@State private var isRunning: Bool = false
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text(String(format: "%.2f", timeElapsed))
.font(.system(size: 50, weight: .medium, design: .monospaced))
.padding()
HStack {
Button(action: {
isRunning ? stopTimer() : startTimer()
}) {
Text(isRunning ? "Stop" : "Start")
.font(.title2)
.padding()
.background(isRunning ? Color.red : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
resetTimer()
}) {
Text("Reset")
.font(.title2)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
.padding()
.onReceive(timer) { _ in
if isRunning {
timeElapsed += 0.01
}
}
}
func startTimer() {
isRunning = true
}
func stopTimer() {
isRunning = false
}
func resetTimer() {
isRunning = false
timeElapsed = 0.0
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Explanation:
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
creates a timer that fires every 0.01 seconds..onReceive(timer) { _ in ... }
sets up a subscriber to the timer.- Inside the
onReceive
closure, we check if the timer is running. If so, we incrementtimeElapsed
by 0.01.
Improving UI with Lap Times (Stopwatch)
Let’s add the functionality to record lap times, transforming our app into a full-fledged stopwatch.
Step 3: Add Lap Times
import SwiftUI
struct ContentView: View {
@State private var timeElapsed: Double = 0.0
@State private var isRunning: Bool = false
@State private var lapTimes: [Double] = []
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text(String(format: "%.2f", timeElapsed))
.font(.system(size: 50, weight: .medium, design: .monospaced))
.padding()
HStack {
Button(action: {
isRunning ? stopTimer() : startTimer()
}) {
Text(isRunning ? "Stop" : "Start")
.font(.title2)
.padding()
.background(isRunning ? Color.red : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
resetTimer()
}) {
Text("Reset")
.font(.title2)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
recordLap()
}) {
Text("Lap")
.font(.title2)
.padding()
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(10)
}
}
List {
ForEach(lapTimes.indices.reversed(), id: .self) { index in
HStack {
Text("Lap \(lapTimes.count - index)")
Spacer()
Text(String(format: "%.2f", lapTimes[index]))
}
}
}
.listStyle(PlainListStyle())
}
.padding()
.onReceive(timer) { _ in
if isRunning {
timeElapsed += 0.01
}
}
}
func startTimer() {
isRunning = true
}
func stopTimer() {
isRunning = false
}
func resetTimer() {
isRunning = false
timeElapsed = 0.0
lapTimes.removeAll()
}
func recordLap() {
lapTimes.append(timeElapsed)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Explanation:
@State private var lapTimes: [Double] = []
declares an array to hold the lap times.- A new button “Lap” is added, which calls the
recordLap
function. - The
recordLap
function appends the currenttimeElapsed
to thelapTimes
array. - A
List
displays the recorded lap times in reverse order, so the latest lap appears at the top.
Building a Countdown Timer
Let’s transform our app into a countdown timer where users can set a specific time to count down from.
Step 4: Implement Countdown Timer
import SwiftUI
struct ContentView: View {
@State private var timeRemaining: Int = 60
@State private var isRunning: Bool = false
@State private var timerInput: String = "60"
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text("\(timeRemaining)")
.font(.system(size: 50, weight: .medium, design: .monospaced))
.padding()
HStack {
Text("Set Time:")
TextField("Seconds", text: $timerInput)
.keyboardType(.numberPad)
.frame(width: 80)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
HStack {
Button(action: {
isRunning ? stopTimer() : startTimer()
}) {
Text(isRunning ? "Stop" : "Start")
.font(.title2)
.padding()
.background(isRunning ? Color.red : Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: {
resetTimer()
}) {
Text("Reset")
.font(.title2)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
.padding()
.onReceive(timer) { _ in
if isRunning {
if timeRemaining > 0 {
timeRemaining -= 1
} else {
stopTimer()
}
}
}
}
func startTimer() {
if let time = Int(timerInput) {
timeRemaining = time
}
isRunning = true
}
func stopTimer() {
isRunning = false
}
func resetTimer() {
isRunning = false
timeRemaining = 0
timerInput = "60"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Explanation:
- We replace
timeElapsed
withtimeRemaining
and set it as anInt
. - A
TextField
allows the user to input the initial time. - In
startTimer
, we convert the text input to an integer and settimeRemaining
. - The
onReceive
closure decrementstimeRemaining
every second and stops the timer when it reaches zero.
Enhancements
Here are some potential enhancements to consider:
- Improved UI: Add better visual elements such as progress bars, custom fonts, or animations.
- Settings: Allow users to customize timer intervals, display formats, and other options.
- Background Support: Implement background task support so the timer continues even when the app is in the background.
- Alerts: Show a notification when the timer completes.
Conclusion
Building a stopwatch or timer app in SwiftUI is an excellent way to learn about state management, UI updates, and basic app logic. By following the steps outlined in this guide, you’ve created a functional timer and explored advanced features like lap times and countdown functionality. Experiment with different UI elements and functionalities to deepen your understanding and create a personalized app.