feat(ios): plugin Capacitor CoachLiveBridge (Phase 3) — relais WCSession -> /live

CAPPlugin + CAPBridgedPlugin (jsName CoachLiveBridge). Active WCSession au load(),
reçoit didReceiveMessage (temps réel) + didReceiveUserInfo (lot bufferisé) et
relaie au JS via notifyListeners("liveSample"). Enregistré dans
MainViewController.capacitorDidLoad(). Méthode getStatus pour debug JS. Build OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain
2026-05-24 16:27:15 +02:00
parent 518529891b
commit b2e9836b95
3 changed files with 88 additions and 0 deletions

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
01468471ECA28EA66AFE2D6C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB6C76F68679844EC72178F /* Foundation.framework */; };
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */; };
290D0F17978B7EF3DDB3A792 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A06CDFF5B4F7852B6329862 /* ContentView.swift */; };
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
3E6740C521EF14892687BCF7 /* CoachWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 2517374227A4072A4D6AFC15 /* CoachWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -71,6 +72,7 @@
8AC9C9B9971F55027D227A06 /* WorkoutManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorkoutManager.swift; sourceTree = "<group>"; };
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>"; };
A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachLiveBridge.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>"; };
D1BD5B1990CF3F2B9C5D0002 /* CoachWorkoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachWorkoutKit.swift; sourceTree = "<group>"; };
@@ -144,6 +146,7 @@
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
A9DF3346C49A690C3C3DF2F3 /* CoachLiveBridge.swift */,
);
path = App;
sourceTree = "<group>";
@@ -284,6 +287,7 @@
F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */,
88F28EB12FBB330E00E8306E /* CoachWorkoutObserver.swift in Sources */,
E2CE6C2AA1DF4F3CAD6E0001 /* MainViewController.swift in Sources */,
21B242F8E61FB07C476C477C /* CoachLiveBridge.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -0,0 +1,80 @@
// CoachLiveBridge.swift
// Plugin Capacitor (côté iPhone) : reçoit les samples de séance poussés par
// l'app watchOS CoachWatch via WatchConnectivity et les relaie au JS via
// l'event "liveSample" (consommé par la page web /live).
//
// Enregistrement : MainViewController.capacitorDidLoad() ->
// bridge?.registerPluginInstance(CoachLiveBridgePlugin())
//
// Usage JS :
// const CoachLiveBridge = registerPlugin('CoachLiveBridge');
// CoachLiveBridge.addListener('liveSample', (s) => { ... });
// // s.heartRate, s.activeEnergyKcal, s.distanceMeters, s.elapsedSec, s.ts
import Foundation
import Capacitor
import WatchConnectivity
@objc(CoachLiveBridgePlugin)
public class CoachLiveBridgePlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "CoachLiveBridgePlugin"
public let jsName = "CoachLiveBridge"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise)
]
override public func load() {
super.load()
guard WCSession.isSupported() else {
CAPLog.print("⚡️ CoachLiveBridge: WCSession non supporté sur cet appareil")
return
}
let session = WCSession.default
session.delegate = self
session.activate()
}
@objc func getStatus(_ call: CAPPluginCall) {
let session = WCSession.default
call.resolve([
"supported": WCSession.isSupported(),
"activated": session.activationState == .activated,
"paired": session.isPaired,
"watchAppInstalled": session.isWatchAppInstalled,
"reachable": session.isReachable
])
}
private func emit(_ sample: [String: Any]) {
DispatchQueue.main.async { [weak self] in
self?.notifyListeners("liveSample", data: sample)
}
}
}
extension CoachLiveBridgePlugin: WCSessionDelegate {
public func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {}
public func sessionDidBecomeInactive(_ session: WCSession) {}
public func sessionDidDeactivate(_ session: WCSession) {
// Réactiver pour les sessions futures (ex. changement d'Apple Watch).
WCSession.default.activate()
}
// Temps réel : un sample envoyé via sendMessage.
public func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
emit(message)
}
// Livraison différée : lot de samples envoyé via transferUserInfo.
public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
if let samples = userInfo["samples"] as? [[String: Any]] {
for sample in samples { emit(sample) }
} else {
emit(userInfo)
}
}
}

View File

@@ -18,6 +18,10 @@ class MainViewController: CAPBridgeViewController {
// Plugin natif observer HealthKit détecte fin de séance Watch et
// push notif locale "🎉 Séance terminée" (cf. CoachWorkoutObserver.swift).
bridge?.registerPluginInstance(CoachWorkoutObserverPlugin())
// Plugin natif live workout reçoit les samples temps réel de l'app
// watchOS CoachWatch via WatchConnectivity, relaie au JS (event
// "liveSample") pour la page /live (cf. CoachLiveBridge.swift).
bridge?.registerPluginInstance(CoachLiveBridgePlugin())
}
override func viewDidLoad() {