Files
workouts/Workouts/AppServices.swift
T
rzen a4ed4df756 Make Apple Health workout recording watch-only
The phone no longer writes estimated Health workouts: the watch, which
runs the live session, is the sole recorder. Replaces WorkoutHealthWriter
with a WorkoutHealthDeleter that only removes a legacy phone-estimate
workout when its record is deleted here, drops the MET calorie table and
the phone's write/read Health scopes, and keeps phoneEstimate decodable
for existing documents.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
2026-07-08 07:56:19 -04:00

50 lines
1.9 KiB
Swift

import Foundation
import Observation
import SwiftData
/// Composition root for the iOS app. Owns the SwiftData cache container and the
/// iCloud sync engine, and drives the one-shot launch sequence. Injected into the
/// view tree via `.environment(...)`.
@Observable
@MainActor
final class AppServices {
let container: ModelContainer
let syncEngine: SyncEngine
let watchBridge: PhoneConnectivityBridge
let workoutLauncher = WorkoutLauncher()
let workoutHealthDeleter = WorkoutHealthDeleter()
/// Ephemeral live-run state fed by the watch, observed by the mirror UI. Not persisted.
let liveRunState: LiveRunState
private var bootstrapTask: Task<Void, Never>?
init() {
let container = WorkoutsModelContainer.make()
self.container = container
self.syncEngine = SyncEngine(container: container)
let liveRunState = LiveRunState()
self.liveRunState = liveRunState
self.watchBridge = PhoneConnectivityBridge(container: container, syncEngine: syncEngine, liveRunState: liveRunState)
#if DEBUG
if ScreenshotSeed.isActive { ScreenshotSeed.populate(container.mainContext) }
#endif
}
/// Launch step: resolve iCloud and reconcile the cache. Idempotent — repeated
/// callers await the same one-shot task.
func bootstrap() async {
if let bootstrapTask { await bootstrapTask.value; return }
let task = Task { @MainActor [weak self] in
guard let self else { return }
await self.syncEngine.connect()
self.watchBridge.activate()
// Past the iCloud gate: request the workout-share scope so the user can still
// delete legacy phone-estimate workouts from Health when deleting them here.
self.workoutHealthDeleter.authorizeIfNeeded()
}
bootstrapTask = task
await task.value
}
}