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.
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.// 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.@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".CustomCounter(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.
// ✅ 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).
@main
struct MyWatchApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// 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)
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete(perform: delete)
}
.listStyle(.carousel) // For focused content
.listStyle(.elliptical) // For browsing
import 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 |
// 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
}
}
import 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)
}
}
import 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
])
}
}
@State private var crownValue = 0.0
ScrollView {
// Content
}
.focusable()
.digitalCrownRotation($crownValue)
WKInterfaceDevice.current().play(.click)
WKInterfaceDevice.current().play(.success)
WKInterfaceDevice.current().play(.failure)
import WatchKit
NowPlayingView() // Built-in now playing controls
import 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 |
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer