FitTrack — Implementation Guide

This guide assumes you’ve finished SkyWatch and are comfortable with SwiftUI + Swift concurrency. Total estimated time: 50–70 hours.

Day 1 — Project + HealthKit auth

Step 1. Create iOS + watchOS targets

In Xcode: File → New → Project → iOS → App → Product Name FitTrack. Then File → New → Target → watchOS → Watch App → embed in companion iOS app.

Step 2. Capabilities

Both targets:

    • Capability → HealthKit.
  • iOS target: also + iCloud → CloudKit, container iCloud.com.yourorg.fittrack.
  • iOS target: + App Groupsgroup.com.yourorg.fittrack (for widget data sharing).

Step 3. Info.plist usage strings

<key>NSHealthShareUsageDescription</key>
<string>FitTrack reads your heart rate, steps, distance, and active energy to show trends and power your workout history.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>FitTrack writes the workouts you log so they appear in Apple Health and contribute to your move ring.</string>

Step 4. Request authorization

import HealthKit

actor HealthAuth {
    let store: HKHealthStore
    init() { self.store = HKHealthStore() }

    func request() async throws {
        guard HKHealthStore.isHealthDataAvailable() else {
            throw HealthError.unavailable
        }
        let read: Set<HKObjectType> = [
            .quantityType(forIdentifier: .heartRate)!,
            .quantityType(forIdentifier: .stepCount)!,
            .quantityType(forIdentifier: .activeEnergyBurned)!,
            .quantityType(forIdentifier: .distanceWalkingRunning)!,
            HKObjectType.workoutType()
        ]
        let write: Set<HKSampleType> = [HKObjectType.workoutType()]
        try await store.requestAuthorization(toShare: write, read: read)
    }
}

Checkpoint: launch the app, see the HealthKit permission sheet, allow all, no crash.

Day 2 — SwiftData + CloudKit setup

Step 5. Define Workout model (see architecture.md)

Step 6. Configure ModelContainer

@main
struct FitTrackApp: App {
    let container: ModelContainer = {
        let config = ModelConfiguration(
            cloudKitDatabase: .private("iCloud.com.yourorg.fittrack")
        )
        return try! ModelContainer(for: Workout.self, WorkoutTag.self, configurations: config)
    }()

    var body: some Scene {
        WindowGroup { ContentView() }
            .modelContainer(container)
    }
}

Step 7. Deploy schema to CloudKit

  1. Run the app once on a device signed into iCloud — SwiftData seeds the dev container.
  2. CloudKit Dashboard → your container → Schema → Deploy to Production.

Checkpoint: insert a Workout on one device, see it appear on another within 30 s.

Day 3–4 — HealthKit query stream

Step 8. Build the HealthQueryStream actor

See the implementation in architecture.md. Add to HealthKitBridge package.

Step 9. Consume in a SwiftUI view

struct HeartRateLiveView: View {
    @State private var latestBPM: Double?
    let store = HKHealthStore()

    var body: some View {
        Text(latestBPM.map { "\(Int($0)) bpm" } ?? "—")
            .task {
                let stream = HealthQueryStream<HKQuantitySample>(
                    store: store,
                    sampleType: .quantityType(forIdentifier: .heartRate)!
                )
                do {
                    for try await samples in stream.samples() {
                        if let last = samples.last {
                            latestBPM = last.quantity.doubleValue(for: HKUnit(from: "count/min"))
                        }
                    }
                } catch {
                    // log
                }
            }
    }
}

Checkpoint: while wearing an Apple Watch, the value updates within seconds.

Day 5 — Workout logging on iPhone

Step 10. Log workout form

A SwiftUI Form with activity type picker, date pickers, duration stepper, notes field, optional photo picker.

Step 11. Save to both stores

func saveWorkout(...) async throws {
    let id = UUID()

    // 1. HealthKit
    let hkWorkout = HKWorkout(
        activityType: activityType.hkType,
        start: startDate, end: startDate.addingTimeInterval(duration),
        duration: duration,
        totalEnergyBurned: nil, totalDistance: nil,
        metadata: [HKMetadataKeyExternalUUID: id.uuidString]
    )
    try await store.save(hkWorkout)

    // 2. SwiftData
    let model = Workout(id: id, activityType: activityType, startDate: startDate, duration: duration)
    model.notes = notes
    modelContext.insert(model)
    try modelContext.save()
}

Both use the same UUID so we can correlate them later.

Checkpoint: log a workout. Open Apple Health → Browse → Workouts. Your workout is there. Open FitTrack on another device — same workout shows up.

Day 6–7 — Watch workout session

Step 12. Set up HKWorkoutSession

@MainActor
final class WatchWorkoutController: NSObject, ObservableObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate {
    @Published var heartRate: Double = 0
    @Published var elapsed: TimeInterval = 0

    var session: HKWorkoutSession?
    var builder: HKLiveWorkoutBuilder?

    func start(activity: HKWorkoutActivityType) {
        let config = HKWorkoutConfiguration()
        config.activityType = activity
        config.locationType = .indoor

        do {
            session = try HKWorkoutSession(healthStore: HKHealthStore(), configuration: config)
            builder = session?.associatedWorkoutBuilder()
            builder?.dataSource = HKLiveWorkoutDataSource(healthStore: HKHealthStore(), workoutConfiguration: config)
            session?.delegate = self
            builder?.delegate = self
            let start = Date()
            session?.startActivity(with: start)
            builder?.beginCollection(withStart: start) { _, _ in }
        } catch {
            // log
        }
    }

    func end() async {
        session?.end()
        try? await builder?.endCollection(at: Date())
        try? await builder?.finishWorkout()
    }

    // delegate methods omitted — collect heart rate samples and update self.heartRate
}

Step 13. Watch UI

Three views: activity picker, in-workout (heart rate big, elapsed, end button), summary.

Checkpoint: start a workout on Watch. Heart rate updates live. End it. The workout appears in Apple Health and in the iPhone FitTrack history within 30 s.

Day 8 — Swift Charts dashboard

Step 14. 30-day heart rate trend

struct HeartRateTrendChart: View {
    let samples: [HRSample]  // pre-aggregated daily averages

    var body: some View {
        Chart(samples) { s in
            LineMark(x: .value("Day", s.date), y: .value("BPM", s.average))
                .interpolationMethod(.catmullRom)
                .foregroundStyle(.pink)
            AreaMark(x: .value("Day", s.date), y: .value("BPM", s.average))
                .foregroundStyle(LinearGradient(colors: [.pink.opacity(0.3), .clear], startPoint: .top, endPoint: .bottom))
        }
        .chartXAxis { AxisMarks(values: .stride(by: .day, count: 7)) }
        .chartYScale(domain: 40...160)
        .frame(height: 220)
    }
}

Aggregate samples in a background ModelActor query, not on the main thread.

Checkpoint: chart renders for 30 days of real data. Sample-rich days show higher accuracy; sparse days show gaps, not zeros.

Day 9 — Live Activity + Dynamic Island

Step 15. Add the Widget Extension

Already in place from earlier steps. Add an ActivityAttributes:

struct WorkoutActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var heartRate: Int
        var elapsed: TimeInterval
    }
    var activityName: String
}

Step 16. Start the Live Activity from the iPhone

The iPhone receives Watch session start via Watch Connectivity (or via HealthKit’s HKWorkoutSessionMirroredObject on iOS 17+), then:

let attrs = WorkoutActivityAttributes(activityName: "Run")
let initial = WorkoutActivityAttributes.ContentState(heartRate: 0, elapsed: 0)
let activity = try Activity.request(
    attributes: attrs,
    contentState: initial,
    pushType: nil
)

Update every 5 s with activity.update(...).

Step 17. Dynamic Island regions

Implement compact leading/trailing, minimal, and expanded views in the widget bundle. Compact leading: activity icon. Compact trailing: heart rate.

Checkpoint: start a workout on Watch — Live Activity appears on the iPhone Lock Screen and Dynamic Island. Updates every few seconds. Ends when the workout ends.

Day 10 — Complications

Step 18. Modular Compact complication

struct ComplicationProvider: TimelineProvider {
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        // Read latest workout from SwiftData via App Group container
        // ...
        completion(Timeline(entries: entries, policy: .after(.now.addingTimeInterval(3600))))
    }
}

Provide families: .accessoryCircular, .accessoryRectangular, .accessoryCorner.

Checkpoint: long-press the watch face → Edit → add the FitTrack complication → it displays last workout date/icon.

Day 11–12 — Polish + TestFlight

  • Run through hardening-checklist.md.
  • Fastlane lanes for both iOS and watchOS uploads.
  • Privacy Nutrition Label (Health & Fitness, Linked to user, NOT used for tracking).
  • Screenshots for 6.7“, 6.1“, 5.5“, 12.9“ iPad if you support iPad.

Next: Hardening checklist