diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 1cb1c67..f4e2a3f 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */; }; 88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F28EB02FBB330E00E8306E /* CoachWorkoutObserver.swift */; }; + B06AB24B350B221F38D99E17 /* ConnectivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A420239FC6D065C8A599B61C /* ConnectivityManager.swift */; }; C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AC4A07811751FB78AA0002 /* CoachAuth.swift */; }; D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */; }; E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.swift */; }; @@ -67,6 +68,7 @@ 88F28EB02FBB330E00E8306E /* CoachWorkoutObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutObserver.swift; sourceTree = ""; }; 8A06CDFF5B4F7852B6329862 /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; + A420239FC6D065C8A599B61C /* ConnectivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityManager.swift; sourceTree = ""; }; C0AC4A07811751FB78AA0002 /* CoachAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAuth.swift; sourceTree = ""; }; D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = CoachWatch.entitlements; sourceTree = ""; }; D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = ""; }; @@ -151,6 +153,7 @@ 8A06CDFF5B4F7852B6329862 /* ContentView.swift */, 82122ACF7E5A608AD1D55D24 /* Info.plist */, D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */, + A420239FC6D065C8A599B61C /* ConnectivityManager.swift */, ); name = CoachWatch; path = CoachWatch; @@ -287,6 +290,7 @@ files = ( 543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */, 290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */, + B06AB24B350B221F38D99E17 /* ConnectivityManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/App/CoachWatch/ConnectivityManager.swift b/ios/App/CoachWatch/ConnectivityManager.swift new file mode 100644 index 0000000..6e32f3f --- /dev/null +++ b/ios/App/CoachWatch/ConnectivityManager.swift @@ -0,0 +1,76 @@ +import Foundation +import WatchConnectivity + +/// 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 + 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() } + } + } +}