feat(watchos): ConnectivityManager (WCSession) — envoi des samples vers l'iPhone

sendMessage si iPhone joignable, sinon buffer + transferUserInfo (livraison
différée). Délégué WCSession hors-main, hop vers main pour @Published isReachable,
buffer sérialisé sur file dédiée. Build watchsimulator OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain
2026-05-24 16:11:58 +02:00
parent 5d60a4049e
commit 5864cfcbef
2 changed files with 80 additions and 0 deletions

View File

@@ -20,6 +20,7 @@
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */; }; 543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */; };
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F28EB02FBB330E00E8306E /* CoachWorkoutObserver.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 */; }; C0AC4A07811751FB78AA0001 /* CoachAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AC4A07811751FB78AA0002 /* CoachAuth.swift */; };
D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */; }; D1BD5B1990CF3F2B9C5D0001 /* CoachWorkoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */; };
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CE6C2AA1DF4F3CAD6E0002 /* MainViewController.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 = "<group>"; }; 88F28EB02FBB330E00E8306E /* CoachWorkoutObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutObserver.swift; sourceTree = "<group>"; };
8A06CDFF5B4F7852B6329862 /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 8A06CDFF5B4F7852B6329862 /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; 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 = "<group>"; };
C0AC4A07811751FB78AA0002 /* CoachAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAuth.swift; sourceTree = "<group>"; }; C0AC4A07811751FB78AA0002 /* CoachAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachAuth.swift; sourceTree = "<group>"; };
D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = CoachWatch.entitlements; sourceTree = "<group>"; }; D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = CoachWatch.entitlements; sourceTree = "<group>"; };
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = "<group>"; }; D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = "<group>"; };
@@ -151,6 +153,7 @@
8A06CDFF5B4F7852B6329862 /* ContentView.swift */, 8A06CDFF5B4F7852B6329862 /* ContentView.swift */,
82122ACF7E5A608AD1D55D24 /* Info.plist */, 82122ACF7E5A608AD1D55D24 /* Info.plist */,
D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */, D09B589FA572B2C14E8CD835 /* CoachWatch.entitlements */,
A420239FC6D065C8A599B61C /* ConnectivityManager.swift */,
); );
name = CoachWatch; name = CoachWatch;
path = CoachWatch; path = CoachWatch;
@@ -287,6 +290,7 @@
files = ( files = (
543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */, 543A7005429A43F54C0EC61B /* CoachWatchApp.swift in Sources */,
290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */, 290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */,
B06AB24B350B221F38D99E17 /* ConnectivityManager.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -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() }
}
}
}