Logger unified (capturable via simctl) côté watch (envoi) et iPhone (réception) pour valider le pont WCSession. Hook AUTOSIM (env var) démarre le mode test au lancement, pour validation automatisée sur simulateur. Pont prouvé E2E sur sim pair iPhone+Watch : HR identiques watch->iPhone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
Swift
82 lines
2.9 KiB
Swift
import Foundation
|
|
import WatchConnectivity
|
|
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() }
|
|
}
|
|
}
|
|
}
|