| name | watchos-patterns |
| description | watchOS platform patterns: Digital Crown, Always On Display, battery constraints, WKApplicationDelegate lifecycle, wrist detection. Use when working on shared watchOS patterns related to watchos patterns. |
watchOS Platform Patterns
Digital Crown Input
The Digital Crown is the primary input for value adjustment on Apple Watch.
@State private var crownValue: Double = 0
ScrollView {
content
}
.digitalCrownRotation($crownValue)
@State private var volume: Double = 50
VolumeView(level: volume)
.digitalCrownRotation(
$volume,
from: 0,
through: 100,
by: 1,
sensitivity: .medium,
isContinuous: false,
isHapticFeedbackEnabled: true
)
Crown Sensitivity
.low — large physical rotation per unit (precise adjustment)
.medium — balanced (default for most use cases)
.high — small physical rotation per unit (fast scrolling)
Focus for Crown
Only the focused view receives Crown events:
@FocusState private var crownFocused: Bool
VStack {
MetricView(value: crownValue)
}
.digitalCrownRotation($crownValue)
.focusable()
.focused($crownFocused)
.onAppear { crownFocused = true }
Always On Display
watchOS apps should support Always On Display when the wrist is lowered.
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var body: some View {
VStack {
if isLuminanceReduced {
Text(Date.now, style: .time)
.font(AppTheme.Fonts.title)
} else {
DetailedContentView()
}
}
}
Always On Display Rules
- Check
\.isLuminanceReduced to detect Always On state
- Reduce visual complexity: hide animations, secondary info, and interactive elements
- Use
TimelineView(.everyMinute) for clock-like updates in reduced mode
- Avoid bright colors — use dimmer variants in reduced luminance
- Stop all timers and animations when
isLuminanceReduced == true
TimelineView(.everyMinute) { context in
if isLuminanceReduced {
Text(context.date, style: .time)
} else {
LiveActivityView()
}
}
Battery & Performance Constraints
Apple Watch has very limited battery — every CPU/GPU cycle matters.
Rules
- Avoid continuous animations (use
.animation only for state transitions)
- Minimize network requests — batch and cache aggressively
- No background processing unless using
WKApplicationRefreshBackgroundTask
- Prefer
TimelineView over Timer for periodic updates
- Use
.task for async work — it cancels automatically when the view disappears
- Keep view hierarchies shallow (2-3 levels)
- Images should be small and pre-sized for watch dimensions
Background Refresh
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
switch task {
case let refreshTask as WKApplicationRefreshBackgroundTask:
scheduleNextRefresh()
refreshTask.setTaskCompletedWithSnapshot(true)
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
App Lifecycle
WKApplicationDelegate (watchOS app lifecycle)
import WatchKit
class AppDelegate: NSObject, WKApplicationDelegate {
func applicationDidFinishLaunching() {
}
func applicationDidBecomeActive() {
}
func applicationWillResignActive() {
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
}
}
@main
struct MyWatchApp: App {
@WKApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Scene Phases
@Environment(\.scenePhase) var scenePhase
.onChange(of: scenePhase) { _, phase in
switch phase {
case .active:
refreshData()
case .inactive:
saveState()
case .background:
scheduleBackgroundRefresh()
@unknown default:
break
}
}
Extended Runtime Sessions
For workouts or navigation that need to keep running:
import WatchKit
let session = WKExtendedRuntimeSession()
session.start()
session.invalidate()
Wrist Detection Auth Pattern
import LocalAuthentication
func requireAuth() async -> Bool {
let context = LAContext()
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) else {
return false
}
do {
return try await context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: "Access sensitive data"
)
} catch {
return false
}
}
Platform Rules
- Digital Crown is the primary non-touch input — always support it for value adjustment
- Always support
isLuminanceReduced for Always On Display
- Battery is the #1 constraint — no continuous animations, no polling
- Use
WKApplicationDelegate for lifecycle hooks and background tasks
- Keep interactions to 1-2 seconds — the watch is for glances, not sessions
- No UIKit — watchOS is SwiftUI-only (no UIViewController, UIView, etc.)