Countdown Timer
MediumSwiftUISwiftUIApp lifecycleConcurrency 40 min read
Build a countdown timer screen, like the Timer tab in the Clock app. The user picks a duration, then taps Start, Pause, Resume or Reset.
The hard part: the time must stay correct when the app is in the background. iOS stops (suspends) your app soon after the user leaves it. A counter that adds 1 every second also stops. Use dates instead.
@MainActor @Observable
final class CountdownTimer {
func start(now: Date = .now) // also resumes after a pause
func pause(now: Date = .now)
func reset()
func remaining(at now: Date) -> TimeInterval
}Example flow
- The user picks "1 min" and taps Start. The screen shows
01:00, then00:59,00:58… - At
00:45the user taps Pause. The time stops at00:45. - The user taps Resume. It continues from
00:45. - At
00:40the user goes to the home screen for 30 seconds. - The user comes back. The screen shows
00:10right away, not00:40. - At
00:00it says "Time is up" and the phone gives a short vibration.
Requirements
- Duration presets: 10 sec, 1 min, 5 min, 25 min. You cannot change them while running or paused.
- Buttons: Start (idle or finished), Pause (running), Resume (paused), Reset (not idle).
- Show the time as
mm:ss. Showh:mm:ssfor an hour or more. - Round up: with 0.4 s left, show
00:01, not00:00. - A ring shows the progress.
- Store the end date while running. Do not count ticks.
- When the app comes back to the foreground, update at once.
- When time is up, show "Time is up" and play a haptic.
What a good answer handles
- Pause. A paused timer has no end date. Store the remaining time instead.
- Battery. Do not redraw the screen while paused.
- Testability. Pass
nowinto the model so unit tests do not wait for real time. - VoiceOver. Read "Time remaining, 1 minute, 5 seconds", not "zero one colon zero five". Announce "Time is up".
- Background alert. Say how you would alert the user if the time ends while the app is in the background.
Notes
- Target iOS 17 or later. Use
@Observable,TimelineViewand.task(id:). Timeris allowed for redrawing, but it must not be the source of truth for the time.
Starter code
Copy this into a new Xcode project (or Swift Playgrounds) and build from here.
import SwiftUI
// MARK: - Model
@MainActor
@Observable
final class CountdownTimer {
// TODO: store dates, not a tick count.
// Hint: idle, running(endDate:), paused(remaining:), finished.
private(set) var duration: TimeInterval
init(duration: TimeInterval = 60) {
self.duration = max(duration, 1)
}
func start(now: Date = .now) {
// TODO: start, or resume after a pause
}
func pause(now: Date = .now) {
// TODO
}
func reset() {
// TODO
}
func remaining(at now: Date) -> TimeInterval {
// TODO
duration
}
}
// MARK: - View
struct CountdownView: View {
@State private var timer = CountdownTimer(duration: 60)
var body: some View {
VStack(spacing: 32) {
// TODO: redraw while running (TimelineView), show mm:ss and a progress ring.
Text("01:00")
.font(.system(size: 56, weight: .semibold, design: .rounded))
.monospacedDigit()
// TODO: duration presets, Start / Pause / Resume, Reset,
// finish detection, background handling, accessibility.
HStack {
Button("Reset") { timer.reset() }
Button("Start") { timer.start() }
}
}
.padding()
}
}
#Preview {
CountdownView()
}Solution
Try to build it first. Then compare your design with ours.