Library / Outcomes & notifications
Countdown 3-2-1-Go
A workout or recording countdown: a tick per second, then a distinct stronger pattern on "go".
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)
} 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()
}
}
}