import Foundation import WatchConnectivity import WidgetKit import os private let connLog = Logger(subsystem: "ch.hypnotruck.coach.watchkitapp", category: "connectivity") /// Envoie les samples de séance vers l'iPhone. /// - `sendMessage` quand l'iPhone est joignable (temps réel) /// - sinon buffer + `transferUserInfo` (livraison différée garantie) final class ConnectivityManager: NSObject, ObservableObject { static let shared = ConnectivityManager() @Published private(set) var isReachable = false private let queue = DispatchQueue(label: "ch.hypnotruck.coach.connectivity") private var buffer: [[String: Any]] = [] private override init() { super.init() guard WCSession.isSupported() else { return } let session = WCSession.default session.delegate = self session.activate() } func sendSample(_ payload: [String: Any]) { queue.async { let session = WCSession.default let hr = (payload["heartRate"] as? Double) ?? -1 connLog.info("sendSample activated=\(session.activationState == .activated, privacy: .public) reachable=\(session.isReachable, privacy: .public) hr=\(hr, privacy: .public)") guard session.activationState == .activated else { self.buffer.append(payload) return } if session.isReachable { session.sendMessage(payload, replyHandler: nil) { [weak self] _ in // échec d'envoi temps réel -> bascule en livraison différée self?.queue.async { self?.buffer.append(payload) self?.flushLocked() } } } else { self.buffer.append(payload) self.flushLocked() } } } /// À appeler sur `queue` uniquement. private func flushLocked() { guard !buffer.isEmpty else { return } let session = WCSession.default guard session.activationState == .activated else { return } session.transferUserInfo(["samples": buffer]) buffer.removeAll() } private func updateReachable(_ value: Bool) { DispatchQueue.main.async { [weak self] in self?.isReachable = value } } } extension ConnectivityManager: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { updateReachable(session.isReachable) if activationState == .activated { queue.async { [weak self] in self?.flushLocked() } } } func sessionReachabilityDidChange(_ session: WCSession) { let reachable = session.isReachable updateReachable(reachable) if reachable { queue.async { [weak self] in self?.flushLocked() } } } // ─── Réception du snapshot widget (iPhone → Watch) ─── // L'iPhone (CoachWidgetBridge) pousse les données du jour ; on les persiste // dans l'App Group de CETTE montre et on recharge les complications. func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { storeWidgetSnapshot(applicationContext) } func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { storeWidgetSnapshot(userInfo) } private func storeWidgetSnapshot(_ dict: [String: Any]) { guard let data = dict["widgetSnapshot"] as? Data, let snapshot = try? JSONDecoder().decode(CoachWidgetSnapshot.self, from: data) else { return } CoachWidgetStore.save(snapshot) connLog.info("widgetSnapshot reçu de l'iPhone -> complications rechargées") DispatchQueue.main.async { WidgetCenter.shared.reloadAllTimelines() } } }