import Foundation import SwiftData import WatchConnectivity /// Phone side of the iPhone↔Watch bridge. The phone owns iCloud Drive; the watch /// is a thin remote that round-trips through it: /// • Phone → Watch: pushes all splits + recent workouts as the latest /// application context whenever the cache changes (local or remote). /// • Watch → Phone: receives an updated `WorkoutDocument` and applies it via the /// SyncEngine write path (file → observer → cache → push back). @MainActor final class PhoneConnectivityBridge: NSObject { private let container: ModelContainer private let syncEngine: SyncEngine private var session: WCSession? private var context: ModelContext { container.mainContext } init(container: ModelContainer, syncEngine: SyncEngine) { self.container = container self.syncEngine = syncEngine super.init() } func activate() { guard WCSession.isSupported() else { return } let session = WCSession.default session.delegate = self session.activate() self.session = session // Push fresh state to the watch whenever the cache changes. syncEngine.onCacheChanged = { [weak self] in self?.pushAll() } } /// Sends the current splits + most-recent workouts to the watch. func pushAll() { guard let session, session.activationState == .activated, session.isPaired, session.isWatchAppInstalled else { return } let splits = (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.order)]))) ?? [] var wDesc = FetchDescriptor(sortBy: [SortDescriptor(\.start, order: .reverse)]) wDesc.fetchLimit = 25 let workouts = (try? context.fetch(wDesc)) ?? [] let restSeconds = UserDefaults.standard.object(forKey: WCPayload.restSecondsKey) as? Int ?? 45 let payload = WCPayload.encodeState( splits: splits.map(SplitDocument.init(from:)), workouts: workouts.map(WorkoutDocument.init(from:)), restSeconds: restSeconds ) try? session.updateApplicationContext(payload) } /// Parse the (non-Sendable) WC dictionary in the nonisolated delegate context, /// then hop to the MainActor with only Sendable values. nonisolated private func route(_ dict: [String: Any]) { switch dict[WCPayload.typeKey] as? String { case WCPayload.requestSyncType: Task { @MainActor in self.pushAll() } case WCPayload.workoutUpdateType: if let doc = WCPayload.decodeWorkoutUpdate(dict) { Task { @MainActor in await self.syncEngine.ingestFromWatch(doc) } } default: break } } } // MARK: - WCSessionDelegate extension PhoneConnectivityBridge: WCSessionDelegate { nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { Task { @MainActor in self.pushAll() } } nonisolated func sessionDidBecomeInactive(_ session: WCSession) {} nonisolated func sessionDidDeactivate(_ session: WCSession) { session.activate() // reactivate for a switched watch } nonisolated func sessionReachabilityDidChange(_ session: WCSession) { if session.isReachable { Task { @MainActor in self.pushAll() } } } nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { route(message) } nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { route(userInfo) } }