Install any skill in seconds. Free to start, no credit card required.
Get Started Free →watchOS development guidance including SwiftUI for Watch, Watch Connectivity, complications, and watch-specific UI patterns. Use for watchOS code review, best practices, or Watch app development.
.claude/skills/rshankras-watchos/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 186% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 193% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 96% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 187% | 0% |
Comprehensive guidance for watchOS app development with SwiftUI, Watch Connectivity, and complications.
Use this skill when the user:
Design for roughly ten seconds of attention: "if you had ten seconds of someone's attention, which information would you surface?" Launch directly into that detail view — chosen by location, recency, or frequency — and make it so unmistakable it needs no title.
| Layout | Use For | Notes | |--------|---------|-------| | Dial | Dense at-a-glance status | Up to 4 corner controls; .scenePadding(.horizontal) to align with the bezel | | Infographic | Charts + metrics | One chart with supporting numbers | | List | Scrollable finding | When the user must locate an item |
NavigationSplitView: always initialize the selection so the app launches straight to detail, and leave the source list untitled.NavigationStack only when neither fits — and hierarchical navigation should remember the last destination across launches.swift// Source List: launch to detail, not the list NavigationSplitView { List(rooms, selection: $selectedRoom) { room in // source list stays untitled Text(room.name) } } detail: { RoomView(room: selectedRoom) } // Initialize selectedRoom (last used / most relevant) so launch lands on detail
.topBarLeading, .topBarTrailing (moves the time to the center), and .bottomBar..font(.system(size: 24)) never scales — use .font(.title3) and friends.lineLimit(1) truncates at accessibility sizes — set the real maximum you support (.lineLimit(3)) or remove the limit.swift@Environment(\.sizeCategory) var sizeCategory var body: some View { if sizeCategory < .extraExtraLarge { PlantViewHorizontal(plant: $plant) // default layout } else { PlantViewVertical(plant: $plant) // stacked layout for large sizes } }
NavigationLink combines its children's accessibility automatically — don't add extra grouping inside one; the whole row becomes a single element (WWDC21 10223)..accessibilityLabel("Watering in five days") instead of "Drop, image. Five days." Label icon-only buttons too: .accessibilityLabel("Log \(task.name)") → "Log watering, button".swiftCustomCounter(value: value, increment: increment, decrement: decrement) .accessibilityElement() // drops the +/- buttons as separate stops .accessibilityAdjustableAction { direction in switch direction { case .increment: increment() // swipe up case .decrement: decrement() // swipe down default: break } } .accessibilityLabel("\(task.name) frequency") .accessibilityValue("\(value) days")
Hand gestures drive the watch with zero screen touches: clench = tap, double-clench = action menu, pinch = next element, double-pinch = previous (WWDC21 10223). A cursor focuses only interactive elements — Button, Toggle, NavigationLink, views with tap gestures, accessibility actions, or actionable traits; static text and disabled elements are skipped.
swift// ✅ static text whose parent owns the tap gesture — make it a cursor stop FreeDrinkInfoView() .accessibilityRespondsToUserInteraction(true) // ✅ cursor frame == tappable area; enlarge tiny hit targets NavigationLink(destination: EditView()) { Image(systemName: "ellipsis").symbolVariant(.circle) } .contentShape(Circle().scale(1.5))
VoiceOver custom actions appear in the AssistiveTouch action menu automatically. Supply a real icon via the Label form of .accessibilityAction { } label: { Label("Edit", systemImage: "ellipsis.circle") } — otherwise the menu falls back to the first letter of the action name (WWDC21 10223).
swift@main struct MyWatchApp: App { var body: some Scene { WindowGroup { ContentView() } } }
swift// Use NavigationStack (watchOS 9+) NavigationStack { List { NavigationLink("Item 1", value: Item.one) NavigationLink("Item 2", value: Item.two) } .navigationDestination(for: Item.self) { item in ItemDetailView(item: item) } } // TabView for main sections TabView { HomeView() ActivityView() SettingsView() } .tabViewStyle(.verticalPage)
swiftList { ForEach(items) { item in ItemRow(item: item) } .onDelete(perform: delete) } .listStyle(.carousel) // For focused content .listStyle(.elliptical) // For browsing
swiftimport WatchConnectivity @Observable final class WatchConnectivityManager: NSObject, WCSessionDelegate { static let shared = WatchConnectivityManager() private(set) var isReachable = false override init() { super.init() if WCSession.isSupported() { WCSession.default.delegate = self WCSession.default.activate() } } // Required delegate methods func session(_ session: WCSession, activationDidCompleteWith state: WCSessionActivationState, error: Error?) { isReachable = session.isReachable } #if os(iOS) func sessionDidBecomeInactive(_ session: WCSession) {} func sessionDidDeactivate(_ session: WCSession) { WCSession.default.activate() } #endif }
| Method | Use Case | Delivery | |--------|----------|----------| | updateApplicationContext | Latest state (settings) | Overwrites previous | | sendMessage | Real-time, both apps active | Immediate | | transferUserInfo | Queued data | Guaranteed, in order | | transferFile | Large data | Background transfer |
swift// Application Context (most common) func updateContext(_ data: [String: Any]) throws { try WCSession.default.updateApplicationContext(data) } // Real-time messaging func sendMessage(_ message: [String: Any]) { guard WCSession.default.isReachable else { return } WCSession.default.sendMessage(message, replyHandler: nil) } // Receiving data func session(_ session: WCSession, didReceiveApplicationContext context: [String: Any]) { Task { @MainActor in // Update UI with received data } }
swiftimport ClockKit struct ComplicationController: CLKComplicationDataSource { func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) { let descriptor = CLKComplicationDescriptor( identifier: "myComplication", displayName: "My App", supportedFamilies: [.circularSmall, .modularSmall, .graphicCircular] ) handler([descriptor]) } func getCurrentTimelineEntry( for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void ) { let template = makeTemplate(for: complication.family) let entry = CLKComplicationTimelineEntry(date: .now, complicationTemplate: template) handler(entry) } }
swiftimport WidgetKit import SwiftUI struct MyComplication: Widget { var body: some WidgetConfiguration { StaticConfiguration( kind: "MyComplication", provider: ComplicationProvider() ) { entry in ComplicationView(entry: entry) } .configurationDisplayName("My Complication") .supportedFamilies([ .accessoryCircular, .accessoryRectangular, .accessoryCorner, .accessoryInline ]) } }
swift@State private var crownValue = 0.0 ScrollView { // Content } .focusable() .digitalCrownRotation($crownValue)
swiftWKInterfaceDevice.current().play(.click) WKInterfaceDevice.current().play(.success) WKInterfaceDevice.current().play(.failure)
swiftimport WatchKit NowPlayingView() // Built-in now playing controls
swiftimport HealthKit @Observable class WorkoutManager { let healthStore = HKHealthStore() var session: HKWorkoutSession? var builder: HKLiveWorkoutBuilder? func startWorkout(type: HKWorkoutActivityType) async throws { let config = HKWorkoutConfiguration() config.activityType = type config.locationType = .outdoor session = try HKWorkoutSession(healthStore: healthStore, configuration: config) builder = session?.associatedWorkoutBuilder() session?.startActivity(with: .now) try await builder?.beginCollection(at: .now) } }
@Observable over ObservableObject (watchOS 10+)Choose the right reference file based on what the user needs:
What are you building?
|
+- iPhone <-> Watch data sync
| -> watch-connectivity.md
| +- Session management, application context, real-time messaging
| +- File transfers, offline caching, complication push updates
|
+- Watch face complications
| -> complications.md
| +- ClockKit (legacy) vs WidgetKit (modern) complications
| +- Migration from ClockKit to WidgetKit
| +- Complication families (circular, rectangular, corner, inline)
| +- Timeline providers, reload strategies, gauges
|
+- Health / fitness / workout tracking
| -> health-fitness.md
| +- HealthKit authorization and data types
| +- HKWorkoutSession and HKLiveWorkoutBuilder
| +- Real-time heart rate, calories, distance
| +- Extended Runtime sessions, route tracking
|
+- watchOS widgets / Smart Stack
| -> widgets-for-watch.md
| +- Smart Stack configuration and relevance
| +- Cross-platform widget sharing (iOS + watchOS)
| +- watchOS-specific design (dark background, small screen)
|
+- General watchOS app development
-> This file (SKILL.md)
+- Design rules: ten-second test, layouts, navigation model, action buttons
+- App structure, navigation, lists
+- Digital Crown, haptics, Now Playing| File | Content | |------|---------| | watch-connectivity.md | iPhone <-> Watch sync, session management, data transfer, offline caching | | complications.md | ClockKit to WidgetKit migration, complication families, timeline providers, gauges | | health-fitness.md | HealthKit, workout sessions, heart rate, Extended Runtime, route tracking, privacy | | widgets-for-watch.md | Smart Stack widgets, relevance, cross-platform sharing, watchOS design |
Other measured skills in the registry, with their headline benchmark lift.