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 Groups →
group.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
- Run the app once on a device signed into iCloud — SwiftData seeds the dev container.
- 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