Library / Outcomes & notifications

Countdown 3-2-1-Go

A workout or recording countdown: a tick per second, then a distinct stronger pattern on "go".

0ms 135ms 270ms 405ms 540ms
Feel
Three identical light ticks a second apart, resolved by a double-hit "go" that clearly breaks the rhythm.
APIs
CHHapticPattern (scheduled events)UIImpactFeedbackGenerator
Minimum OS
iOS 13+
Raw .md
Core Haptics (single scheduled pattern)
import CoreHaptics

/// Plays the whole countdown as ONE pattern — Core Haptics keeps the
/// timing sample-accurate, unlike chained Timer callbacks.
func playCountdown(engine: CHHapticEngine) throws {
    func tick(at t: TimeInterval, intensity: Float, sharpness: Float) -> CHHapticEvent {
        CHHapticEvent(eventType: .hapticTransient, parameters: [
            CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity),
            CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness),
        ], relativeTime: t)
    }

    let events = [
        tick(at: 0.0, intensity: 0.5, sharpness: 0.8),  // 3
        tick(at: 1.0, intensity: 0.5, sharpness: 0.8),  // 2
        tick(at: 2.0, intensity: 0.5, sharpness: 0.8),  // 1
        tick(at: 3.0, intensity: 1.0, sharpness: 0.4),  // GO
        tick(at: 3.12, intensity: 1.0, sharpness: 0.4), // GO (double hit)
    ]

    let pattern = try CHHapticPattern(events: events, parameters: [])
    let player = try engine.makePlayer(with: pattern)
    try engine.start()
    try player.start(atTime: CHHapticTimeImmediate)
}
SwiftUI (simple, timer-driven)
import SwiftUI

struct CountdownView: View {
    @State private var count = 3
    @State private var started = false
    let onGo: () -> Void

    var body: some View {
        Text(count > 0 ? "\(count)" : "GO")
            .font(.system(size: 90, weight: .black, design: .rounded))
            .contentTransition(.numericText(countsDown: true))
            .sensoryFeedback(trigger: count) { _, new in
                new > 0 ? .impact(weight: .light, intensity: 0.6) : .success
            }
            .task {
                for _ in 0..<3 {
                    try? await Task.sleep(for: .seconds(1))
                    withAnimation { count -= 1 }
                }
                onGo()
            }
    }
}
Copied