import Foundation import Observation import SwiftData import IndieBackup /// 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 /// Local ZIP backup/restore of the iCloud document tree (`backupRoot` = the /// container's `Documents/`). Restores rebuild the SwiftData cache via /// `SyncEngine.rebuildCache()`; see `AppBackupConfiguration`. let backupController: BackupController let watchBridge: PhoneConnectivityBridge let workoutLauncher = WorkoutLauncher() let workoutHealthDeleter = WorkoutHealthDeleter() /// Speaks exercise instructions aloud (library Speak button + hands-free workout cues). /// Shared so a new exercise's cue cancels whatever the last screen was still saying. let speechAnnouncer = SpeechAnnouncer() /// Owns the active-workout Live Activity (lock screen + Dynamic Island). Driven by the run /// flow's `LiveProgress` frames, so locking the phone mid-set doesn't lose the timer. let liveActivity = WorkoutLiveActivityController() /// Ephemeral live-run state fed by the watch, observed by the mirror UI. Not persisted. let liveRunState: LiveRunState private var bootstrapTask: Task? init() { let container = WorkoutsModelContainer.make() self.container = container let syncEngine = SyncEngine(container: container) self.syncEngine = syncEngine self.backupController = BackupController( configuration: AppBackupConfiguration(syncEngine: syncEngine) ) 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 } // Clear any Live Activity stranded by a previous launch (app killed mid-run). await self.liveActivity.endStaleActivities() 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 } }