Skip to content
All essays
iOSMarch 27, 202511 min

Swift WatchKit: Apple Watch Development

Create apps for Apple Watch with WatchKit. Complications, notifications, and watch-specific features.

Ü
Ümit Uz
Mobile & Full Stack Developer

WatchKit enables you to create apps for Apple Watch. Learn to build watchOS apps with complications and notifications.

WatchKit Setup

Add Watch Target

  1. 1File > New > Target
  2. 2Choose "watchOS App" target
  3. 3Configure watchOS deployment target

Basic Watch App

swift
import WatchKit
import SwiftUI

struct WatchAppView: View {
    var body: some View {
        VStack {
            Text("Hello, Watch!")
                .font(.title)

            Image(systemName: "heart.fill")
                .imageScale(.large)
                .foregroundColor(.red)
        }
    }
}

Complications

Create Complication

swift
import ClockKit

class ComplicationController: NSObject, CLKComplicationProvider {
    func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
        switch complication.family {
        case .modularSmall:
            let entry = createSmallEntry()
            handler(entry)
        case .modularLarge:
            let entry = createLargeEntry()
            handler(entry)
        case .utilitarianSmall:
            let entry = createUtilitarianEntry()
            handler(entry)
        default:
            handler(nil)
        }
    }

    func createSmallEntry() -> CLKComplicationTimelineEntry {
        let template = CLKComplicationTemplateModularSmall()

        let textProvider = CLKSimpleTextProvider(text: "Steps")
        template.body1TextProvider = textProvider

        return CLKComplicationTimelineEntry(date: Date(), template: template)
    }

    func createLargeEntry() -> CLKComplicationTimelineEntry {
        let template = CLKComplicationTemplateModularLarge()

        let header = CLKSimpleTextProvider(text: "Fitness")
        let body1 = CLKSimpleTextProvider(text: "10,000 steps")

        template.headerTextProvider = header
        template.body1TextProvider = body1

        return CLKComplicationTimelineEntry(date: Date(), template: template)
    }

    func createUtilitarianEntry() -> CLKComplicationTimelineEntry {
        let template = CLKComplicationTemplateUtilitarianSmall()

        let textProvider = CLKSimpleTextProvider(text: "Watch App")
        template.bodyTextProvider = textProvider

        return CLKComplicationTimelineEntry(date: Date(), template: template)
    }
}

Health Data

Watch Health Integration

swift
import HealthKit

class WatchHealthManager: ObservableObject {
    private let healthStore = HKHealthStore()

    @Published var stepCount: Int = 0
    @Published var activeEnergy: Double = 0

    func queryHealthData() {
        guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
            return
        }

        let now = Date()
        let startOfDay = Calendar.current.startOfDay(for: now)

        let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)

        let query = HKStatisticsQuery(
            quantityType: stepType,
            quantitySamplePredicate: predicate,
            options: .cumulativeSum
        ) { query, result, error in
            guard let result = result else {
                print("Error fetching steps: \(error)")
                return
            }

            let steps = result.sumQuantity()?.doubleValue(for: .count()) ?? 0

            DispatchQueue.main.async {
                self.stepCount = Int(steps)
            }
        }

        healthStore.execute(query)
    }

    func queryHeartRate() {
        guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
            return
        }

        let now = Date()
        let startTime = Calendar.current.date(byAdding: .hour, value: -1, to: now)!

        let predicate = HKQuery.predicateForSamples(withStart: startTime, end: now, options: .strictStartDate)

        let query = HKSampleQuery(
            sampleType: heartRateType,
            predicate: predicate,
            limit: 1,
            sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
        ) { query, samples, error in
            guard let sample = samples?.first as? HKQuantitySample else {
                return
            }

            let heartRate = sample.quantity.doubleValue(for: .count())

            DispatchQueue.main.async {
                print("Heart Rate: \(heartRate) BPM")
            }
        }

        healthStore.execute(query)
    }
}

Watch Connectivity

Phone Communication

swift
import WatchConnectivity

class WatchConnectivityManager: NSObject, ObservableObject, WCSessionDelegate {
    private let session = WCSession.default

    @Published var isReachable = false
    @Published var isWatchAppInstalled = false

    override init() {
        super.init()
        session.delegate = self
        session.activateSession()
    }

    func session(_ session: WCSession, activationDidCompleteWith activationState state: WCSessionActivationState, error: Error?) {
        if let error = error {
            print("Watch activation error: \(error)")
        } else {
            print("Watch activation complete")
        }
    }

    func sessionReachabilityDidChange(_ session: WCSession) {
        isReachable = session.isReachable
    }

    func session(_ session: WCSession, didChangeCompanionAppInstalled installed: Bool) {
        isWatchAppInstalled = installed
    }

    func transferData(data: Data) {
        let transfer = WCSessionFileTransfer(fileNames: ["data.txt"])
        transfer.transferFile(data, metadata: nil)
    }
}

Watch Notifications

Custom Notifications

swift
import UserNotifications

class WatchNotificationManager {
    func scheduleWatchNotification(title: String, body: String, delay: TimeInterval) {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay, repeats: false)

        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: trigger
        )

        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Error scheduling notification: \(error)")
            }
        }
    }

    func sendInteractiveNotification() {
        let content = UNMutableNotificationContent()
        content.title = "Quick Actions"
        content.body = "Choose an action"
        content.sound = .default
        content.categoryIdentifier = "interactive"

        let acceptAction = UNNotificationAction(
            identifier: "ACCEPT",
            title: "Accept",
            options: .foreground
        )

        let declineAction = UNNotificationAction(
            identifier: "DECLINE",
            title: "Decline",
            options: .destructive
        )

        let category = UNNotificationCategory(
            identifier: "interactive",
            actions: [acceptAction, declineAction],
            intentIdentifiers: []
        )

        UNUserNotificationCenter.current().setNotificationCategories([category])

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)

        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: trigger
        )

        UNUserNotificationCenter.current().add(request)
    }
}

Digital Crown

Crown Input

swift
import Crown

struct CrownControlView: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        ScrollView(offset: $scrollOffset) {
            VStack(spacing: 20) {
                ForEach(0..<100) { i in
                    Text("Item \(i)")
                        .font(.title)
                }
            }
        }
        .focusable(true)
        .digitalCrownRotation($scrollOffset) { value in
            // Handle crown rotation
            print("Crown rotation: \(value)")
        }
    }
}

Force Touch

Haptic Feedback

swift
struct ForceTouchView: View {
    @State private var forcePressed = false

    var body: some View {
        VStack {
            Image(systemName: forcePressed ? "heart.fill" : "heart")
                .font(.largeTitle)
                .foregroundColor(forcePressed ? .red : .gray)
                .scaleEffect(forcePressed ? 1.2 : 1.0)
        }
        .gesture(
            DragGesture(minimumDistance: 0)
                .onChanged { value in
                    // Detect force touch
                    // (Implementation depends on watchOS version)
                }
        )
    }
}

SwiftUI for watchOS

Native Watch Layouts

swift
struct WatchContentView: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 10) {
                Text("Features")
                    .font(.headline)

                Button("Feature 1") {
                    // Action
                }

                Button("Feature 2") {
                    // Action
                }
            }
        }
        .navigationTitle("App")
    }
}

Best Practices

  1. 1Simplicity: Keep watch apps simple
  2. 2Performance: Optimize for watch hardware
  3. 3Battery: Conserve battery life
  4. 4Sync: Use Watch Connectivity
  5. 5Complications: Provide useful complications
  6. 6Notifications: Use notifications effectively
  7. 7Crown: Leverage digital crown
  8. 8Testing: Test on real devices

WatchKit enables powerful Apple Watch experiences!

Related essays

Next essay
Swift Vision Framework: Computer Vision