Watch : afficher la zone cardiaque pendant la séance

L'app Coach de la montre n'affichait que la FC brute — aucune notion de zone
dans `ContentView.swift`. C'est pourtant au poignet que « 142 bpm » a besoin
d'être traduit ; l'écran iPhone portait déjà l'information, mais on ne le
regarde pas en courant.

- `HeartRateZones.swift` : modèle partagé (App + CoachLiveActivity + les deux
  cibles Watch), câblé dans les 4 phases Sources du pbxproj comme
  `CoachWidgetSnapshot.swift`. ⚠️ Les bornes viennent de `/api/cardiac-zones`,
  version FIGÉE et datée — jamais recalculées côté natif, ce serait rouvrir les
  jeux de zones concurrents supprimés le 12/08.
- `zone(for:)` retient la zone la plus HAUTE qui contient la valeur : les
  bornes se chevauchent d'un battement (Z1 100-109, Z2 109-118), et 109 bpm
  doit se lire Z2, pas Z1. Au-delà du haut de Z5, on reste en Z5 — un effort
  maximal n'est pas une absence de zone.
- `ConnectivityManager` publie les zones reçues avec le snapshot et repart du
  dernier connu au lancement (la montre a pu redémarrer). Un snapshot partiel
  n'efface pas celles en place.
- `HeartRateMetric` : pastille « Z3 », valeur colorée, et la plage sous le
  chiffre. ⚠️ La couleur ne porte jamais l'information seule — illisible pour
  un daltonien, et le contraste d'un écran de montre en plein soleil ne se
  prête pas aux nuances. Libellé VoiceOver explicite.

`switch` en `return` explicites plutôt qu'en expression : la syntaxe courte
exige Swift 5.9 pile, et je ne peux pas compiler ici.

⚠️ NON COMPILÉ — build Xcode requis, avec `getSnapshot` (`8a25f36`) en attente.
This commit is contained in:
Sylvain Bettinelli
2026-08-12 19:13:07 +00:00
parent 8a25f3604a
commit eb819b68ca
6 changed files with 190 additions and 1 deletions

View File

@@ -13,6 +13,14 @@ final class ConnectivityManager: NSObject, ObservableObject {
@Published private(set) var isReachable = false
/// Zones cardiaques en vigueur, poussées par l'iPhone avec le snapshot des
/// complications. Sert à traduire la FC en zone pendant la séance.
///
/// Publiée depuis `ConnectivityManager` plutôt que lue à chaque rendu :
/// `CoachWidgetStore.load()` décode du JSON, l'appeler dans un `body`
/// rafraîchi à chaque battement coûterait pour rien.
@Published private(set) var zones: HeartRateZones?
private let queue = DispatchQueue(label: "ch.hypnotruck.coach.connectivity")
private var buffer: [[String: Any]] = []
@@ -22,6 +30,9 @@ final class ConnectivityManager: NSObject, ObservableObject {
let session = WCSession.default
session.delegate = self
session.activate()
// La montre a pu redémarrer depuis le dernier envoi de l'iPhone : on
// repart du dernier snapshot connu plutôt que d'attendre le suivant.
zones = CoachWidgetStore.load()?.zones
}
func sendSample(_ payload: [String: Any]) {
@@ -224,6 +235,9 @@ extension ConnectivityManager: WCSessionDelegate {
CoachWidgetStore.save(snapshot)
connLog.info("widgetSnapshot reçu de l'iPhone -> complications rechargées")
DispatchQueue.main.async {
// Des zones absentes d'un snapshot partiel ne doivent pas effacer
// celles en place : la séance en cours perdrait son repère.
if let received = snapshot.zones { self.zones = received }
WidgetCenter.shared.reloadAllTimelines()
}
}