import Combine import Foundation import WatchConnectivity @MainActor public final class WatchConnectivityService: NSObject, ObservableObject { @Published public private(set) var activationState: WCSessionActivationState = .notActivated @Published public private(set) var isReachable = false @Published public private(set) var lastSynchronizedAt: Date? @Published public private(set) var lastError: String? public var onEnvelope: ((WatchSyncEnvelopeV1) -> Void)? public var onAcknowledgement: ((Set) -> Void)? private let session: WCSession? private let encoder = WatchCoding.encoder private let decoder = WatchCoding.decoder public override init() { session = WCSession.isSupported() ? .default : nil super.init() } public func activate() { guard let session else { return } session.delegate = self session.activate() activationState = session.activationState isReachable = session.isReachable } public func sendLive(_ envelope: WatchSyncEnvelopeV1) { guard let session, session.isReachable else { return } do { let data = try encoder.encode(envelope) session.sendMessageData(data, replyHandler: nil) { [weak self] error in Task { @MainActor in self?.lastError = error.localizedDescription } } } catch { lastError = error.localizedDescription } } /// Guaranteed delivery for durable mutations. WatchConnectivity persists these while offline. public func transfer(_ envelope: WatchSyncEnvelopeV1) { guard let session else { return } do { session.transferUserInfo(["envelope": try encoder.encode(envelope)]) } catch { lastError = error.localizedDescription } } public func flush(outbox: [WatchSyncEnvelopeV1]) { for envelope in outbox { transfer(envelope) } } public func updateApplicationContext(preferences: WatchPreferencesV1) { guard let session else { return } do { try session.updateApplicationContext(["preferences": try encoder.encode(preferences)]) } catch { lastError = error.localizedDescription } } private func receive(_ data: Data) { do { let envelope = try decoder.decode(WatchSyncEnvelopeV1.self, from: data) lastSynchronizedAt = Date() if envelope.kind == .acknowledgement, let acknowledgement = try? envelope.decodePayload(WatchAcknowledgementV1.self) { onAcknowledgement?(Set(acknowledgement.eventIDs)) } else { onEnvelope?(envelope) } } catch { lastError = error.localizedDescription } } } extension WatchConnectivityService: WCSessionDelegate { nonisolated public func session( _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error? ) { Task { @MainActor [weak self] in self?.activationState = activationState self?.isReachable = session.isReachable self?.lastError = error?.localizedDescription } } nonisolated public func sessionReachabilityDidChange(_ session: WCSession) { Task { @MainActor [weak self] in self?.isReachable = session.isReachable } } nonisolated public func session(_ session: WCSession, didReceiveMessageData messageData: Data) { Task { @MainActor [weak self] in self?.receive(messageData) } } nonisolated public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { guard let data = userInfo["envelope"] as? Data else { return } Task { @MainActor [weak self] in self?.receive(data) } } nonisolated public func session( _ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any] ) { guard let data = applicationContext["preferences"] as? Data else { return } Task { @MainActor [weak self] in guard let self, let preferences = try? self.decoder.decode(WatchPreferencesV1.self, from: data) else { return } let payload = try? self.encoder.encode(preferences) if let payload { self.onEnvelope?(.init( deviceID: "iphone", sequence: 0, kind: .preferences, payload: payload )) } self.lastSynchronizedAt = Date() } } }