```html SwiftUI: How to Build a Watch App (iOS 17+, 2026)

How to Build a Watch App in SwiftUI

iOS 17+ watchOS 10+ Xcode 16+ Advanced APIs: WatchKit Updated: May 12, 2026
TL;DR

Add a Watch App target in Xcode, then use SwiftUI's standard App / Scene protocol — WatchKit powers the runtime under the hood. On watchOS 10+, use TabView(.verticalPage) for the swipe-up navigation pattern Apple now recommends.

// WatchApp.swift  (watchOS target)
import SwiftUI

@main
struct SoariasWatchApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        TabView {
            DashboardView()
            StatsView()
        }
        .tabViewStyle(.verticalPage)
    }
}

#Preview {
    ContentView()
}

Full implementation

The watch target uses the same SwiftUI App protocol you already know from iOS. The key differences are layout constraints (smaller canvas, no keyboard), the vertical-paging TabView pattern introduced in watchOS 10, and the Digital Crown — Apple Watch's signature physical input. Below is a complete, self-contained watch app with a dashboard, a detail drill-down, and Digital Crown–driven progress control, all shareable via @AppStorage with its paired iOS app through App Groups.

// ── WatchApp.swift ──────────────────────────────────────────
import SwiftUI
import WatchKit

@main
struct SoariasWatchApp: App {
    @WKApplicationDelegateAdaptor(AppDelegate.self) var delegate

    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}

class AppDelegate: NSObject, WKApplicationDelegate {
    func applicationDidFinishLaunching() {
        // Perform any one-time watch-side setup here.
    }
}

// ── RootView.swift ───────────────────────────────────────────
struct RootView: View {
    var body: some View {
        TabView {
            DashboardView()
                .tag(0)
            GoalView()
                .tag(1)
            HistoryView()
                .tag(2)
        }
        .tabViewStyle(.verticalPage)   // watchOS 10 swipe-up gesture
    }
}

// ── DashboardView.swift ──────────────────────────────────────
struct DashboardView: View {
    @AppStorage("stepCount", store: UserDefaults(suiteName: "group.com.example.soarias"))
    private var steps: Int = 0

    var body: some View {
        NavigationStack {
            VStack(spacing: 8) {
                Text("\(steps)")
                    .font(.system(size: 48, weight: .bold, design: .rounded))
                    .foregroundStyle(.green)
                Text("steps today")
                    .font(.footnote)
                    .foregroundStyle(.secondary)

                NavigationLink("See details") {
                    DetailView(steps: steps)
                }
                .buttonStyle(.borderedProminent)
                .tint(.green)
            }
            .navigationTitle("Soarias")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

// ── GoalView.swift – Digital Crown example ───────────────────
struct GoalView: View {
    @State private var goal: Double = 8000
    @State private var crownValue: Double = 8000

    var progress: Double { min(1, crownValue / goal) }

    var body: some View {
        VStack(spacing: 6) {
            ZStack {
                Circle()
                    .stroke(Color.secondary.opacity(0.3), lineWidth: 8)
                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(Color.blue, style: StrokeStyle(lineWidth: 8, lineCap: .round))
                    .rotationEffect(.degrees(-90))
                    .animation(.easeInOut, value: progress)
                Text("\(Int(crownValue))")
                    .font(.headline)
            }
            .frame(width: 90, height: 90)
            .focusable()
            .digitalCrownRotation(
                $crownValue,
                from: 1000,
                through: 20000,
                by: 500,
                sensitivity: .medium,
                isContinuous: false,
                isHapticFeedbackEnabled: true
            )

            Text("Adjust goal with Crown")
                .font(.system(size: 11))
                .foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
        }
        .padding()
    }
}

// ── HistoryView.swift ────────────────────────────────────────
struct HistoryView: View {
    let samples: [(String, Int)] = [
        ("Mon", 9_234), ("Tue", 6_120), ("Wed", 11_050),
        ("Thu", 7_800), ("Fri", 5_430)
    ]

    var body: some View {
        List(samples, id: \.0) { day, count in
            HStack {
                Text(day).font(.footnote).foregroundStyle(.secondary)
                Spacer()
                Text("\(count)").font(.footnote.bold())
            }
        }
        .listStyle(.carousel)
        .navigationTitle("History")
    }
}

// ── DetailView.swift ─────────────────────────────────────────
struct DetailView: View {
    let steps: Int

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 10) {
                Label("\(steps) steps", systemImage: "figure.walk")
                Label("~\(steps / 1300) km", systemImage: "map")
                Label("\(steps * 4) cal", systemImage: "flame")
            }
            .font(.footnote)
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
        }
        .navigationTitle("Detail")
    }
}

// ── Previews ─────────────────────────────────────────────────
#Preview("Root") {
    RootView()
}

#Preview("Goal / Crown") {
    GoalView()
}

#Preview("History") {
    HistoryView()
}

How it works

  1. @WKApplicationDelegateAdaptor — Bridges WatchKit's delegate lifecycle into the SwiftUI App protocol. Use AppDelegate for tasks like WCSession activation or background session setup that don't belong in a View.
  2. TabView(.verticalPage) — The watchOS 10 navigation paradigm. Users swipe up/down between tabs rather than tapping a bottom bar. Each child view becomes a full-screen page; the small dots indicator is rendered automatically.
  3. .digitalCrownRotation(_:from:through:by:sensitivity:) — Binds the Digital Crown to a Double state value. The isHapticFeedbackEnabled: true flag produces a tactile click every 500-step increment, matching the by: parameter.
  4. @AppStorage with App Groups — Pass a shared UserDefaults suite (group.com.example.*) to both the watch target and its iOS companion. Any write on one side is immediately visible on the other without a WCSession message round-trip for simple values.
  5. .listStyle(.carousel) — watchOS-only list style that curves rows around the bezel, a native "look" that .plain and .insetGrouped don't provide on the watch.

Variants

WidgetKit complication (watch face tile)

Since watchOS 9, complications are built with WidgetKit — the same API used for iOS 17 Lock Screen widgets. Add a Widget Extension target, then use @main with WidgetBundle:

import WidgetKit
import SwiftUI

struct StepsEntry: TimelineEntry {
    let date: Date
    let steps: Int
}

struct StepsProvider: TimelineProvider {
    func placeholder(in context: Context) -> StepsEntry {
        StepsEntry(date: .now, steps: 8_000)
    }
    func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) {
        completion(StepsEntry(date: .now, steps: 9_234))
    }
    func getTimeline(in context: Context, completion: @escaping (Timeline<StepsEntry>) -> Void) {
        let entry = StepsEntry(date: .now, steps: readStepsFromAppGroup())
        let next  = Calendar.current.date(byAdding: .minute, value: 15, to: .now)!
        completion(Timeline(entries: [entry], policy: .after(next)))
    }
    private func readStepsFromAppGroup() -> Int {
        UserDefaults(suiteName: "group.com.example.soarias")?.integer(forKey: "stepCount") ?? 0
    }
}

struct StepsComplicationView: View {
    let entry: StepsEntry
    var body: some View {
        VStack {
            Image(systemName: "figure.walk").font(.title3)
            Text("\(entry.steps / 1000)k").font(.footnote.bold())
        }
        .widgetAccentable()
    }
}

@main
struct SoariasComplications: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "steps", provider: StepsProvider()) { entry in
            StepsComplicationView(entry: entry)
        }
        .configurationDisplayName("Steps")
        .description("Today's step count.")
        .supportedFamilies([.accessoryCircular, .accessoryCorner, .accessoryRectangular])
    }
}

WatchConnectivity real-time sync

For data that needs to arrive instantly (e.g. a timer started on iPhone), use WCSession from WatchConnectivity. Call WCSession.default.sendMessage(_:replyHandler:errorHandler:) from the iOS side and implement session(_:didReceiveMessage:) in your watch WCSessionDelegate. Keep the delegate as a singleton @Observable class and inject it via .environment() for clean propagation through the SwiftUI hierarchy.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a watch app in SwiftUI for iOS 17+ / watchOS 10+.
Use WatchKit (@WKApplicationDelegateAdaptor), TabView(.verticalPage),
digitalCrownRotation, and WidgetKit for a complication.
Share data with the iOS companion via an App Group UserDefaults suite.
Make it accessible (VoiceOver labels on all interactive elements).
Add a #Preview with realistic sample data for each view.

In Soarias's Build phase, paste this prompt after scaffolding your iOS target — Soarias will add the watchOS target, wire up the App Group entitlement in both targets, and generate the full file tree ready for Xcode.

Related

FAQ

Does this work on watchOS 9 / paired iOS 16?

Partially. The App protocol and basic SwiftUI views work from watchOS 7+. However, TabView(.verticalPage) requires watchOS 10, and WidgetKit complications require watchOS 9. Set your watchOS deployment target in Xcode's project settings and guard any watchOS 10-exclusive APIs with @available(watchOS 10, *) to gracefully degrade on older watches.

How do I send data from iPhone to Apple Watch in real time?

Use WatchConnectivity's WCSession. For non-urgent data (settings, cumulative counts), prefer the Application Context (updateApplicationContext(_:)) — it coalesces updates and delivers when the watch wakes. For time-critical messages (a started timer, a command), use sendMessage(_:replyHandler:), which requires both devices to have an active session and the watch app to be in the foreground. App Group UserDefaults is the simplest option for values written infrequently from the iPhone.

What's the UIKit / WatchKit storyboard equivalent?

Before watchOS 7, all Apple Watch apps used WatchKit's storyboard-based interface with WKInterfaceController subclasses — the direct watchOS analog of UIKit's UIViewController. Storyboard-based watch apps still compile but are deprecated. Apple recommends migrating to the SwiftUI App protocol. If you're maintaining an older app, you can incrementally adopt SwiftUI by hosting SwiftUI views inside a WKInterfaceController via WKHostingController<Content>.

Last reviewed: 2026-05-12 by the Soarias team.

```