Une trace GPS ne peut pas être sauvegardée sans séance
Erreur de compilation remontée du Mac : `Value of optional type 'HKWorkout?' must be unwrapped`. Le fichier promettait quelque chose d'impossible. Vérifié à la source ce jour (DocC HealthKit) : la signature est `finishRoute(with workout: HKWorkout, metadata:)` — non optionnelle — et Apple précise « You must have already saved this workout to the HealthKit store ». Il n'existe aucune API pour clore une route orpheline. Le commentaire qui annonçait « la trace sera sauvegardée sans association plutôt que perdue » décrivait un comportement inatteignable. Le vrai recours tient à ce que dit le bug d'Apple : quand la montre est verrouillée, `finishWorkout()` rend `nil` mais la séance EST écrite dans HealthKit — seul l'objet manque. `WorkoutManager` va donc la rechercher : dernier workout écrit par CETTE app (HKSource.default(), sinon une séance de l'app Exercice pourrait récupérer notre trace), croisant les 5 dernières minutes. ⚠️ La fenêtre porte sur le chevauchement, pas sur `startDate` : filtrer sur le début raterait toute séance de plus de quelques minutes — le piège qui avait rendu muettes les notifications de fin de séance côté iPhone. Si rien n'est récupérable, `discardRoute()` jette la trace explicitement et le journalise comme une perte : un builder abandonné sans `discard()` laisse ses données en suspens, et « any further calls to the builder raise an exception ». Le chemin « séance en salle » (aucune position retenue) reste inchangé, volontairement sans `discard()` : c'est le cas le plus fréquent, il fonctionnait, et aucun build ne l'a validé avec cet appel. `HKSource` est écrit en toutes lettres : `predicateForObjects(from:)` a cinq surcharges et un `.default()` abrégé s'y résout mal. swiftc -parse OK, 57 tests Swift verts (inchangés : ces fichiers importent HealthKit, hors de portée de Linux). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -165,21 +165,30 @@ final class LocationTracker: NSObject, ObservableObject {
|
||||
/// pas l'appeler du tout fait perdre **toute** la trace — le builder est
|
||||
/// invalidé à sa libération.
|
||||
///
|
||||
/// ⚠️ `workout` peut être `nil` : `finishWorkout()` réussit mais ne rend
|
||||
/// pas l'objet **quand la montre est verrouillée** (confirmé par un
|
||||
/// ingénieur Apple). On sauvegarde alors la trace sans association plutôt
|
||||
/// que de la jeter — une route orpheline vaut mieux qu'une sortie sans
|
||||
/// parcours. Une route ne peut être associée qu'une fois, et jamais après
|
||||
/// coup.
|
||||
/// ⚠️ **Une trace ne peut PAS être sauvegardée sans workout.** Une version
|
||||
/// antérieure de ce commentaire l'affirmait ; c'est faux, vérifié à la
|
||||
/// source le 21/08 : la signature est
|
||||
/// `finishRoute(with workout: HKWorkout, metadata:)` — non optionnelle —
|
||||
/// et Apple précise « You must have already saved this workout to the
|
||||
/// HealthKit store ». Il n'existe aucune API pour clore une route
|
||||
/// orpheline. Le cas « montre verrouillée », où `finishWorkout()` rend
|
||||
/// `nil` sans erreur, se traite donc **en amont** : `WorkoutManager` va
|
||||
/// rechercher dans HealthKit le workout qui vient d'y être écrit. Ici, si
|
||||
/// aucun workout n'arrive, il ne reste qu'à jeter la trace explicitement
|
||||
/// (`discardRoute()`) — un builder abandonné sans `discard()` laisse ses
|
||||
/// données en suspens.
|
||||
///
|
||||
/// Échoue aussi si aucune position n'a été insérée — cas normal d'une
|
||||
/// Ne fait rien si aucune position n'a été insérée — cas normal d'une
|
||||
/// séance en salle, à ne pas remonter comme une anomalie.
|
||||
func finishRoute(with workout: HKWorkout?) async {
|
||||
func finishRoute(with workout: HKWorkout) async {
|
||||
await flushPendingRoute()
|
||||
guard let builder = routeBuilder else { return }
|
||||
self.routeBuilder = nil
|
||||
|
||||
guard filter.acceptedCount > 0 else {
|
||||
// Séance en salle : rien à clore. Volontairement SANS `discard()` —
|
||||
// c'est le chemin le plus fréquent, et il fonctionnait tel quel ;
|
||||
// on n'y introduit pas un appel qu'aucun build n'a validé.
|
||||
locationLog.info("aucune position retenue : pas de trace a clore")
|
||||
return
|
||||
}
|
||||
@@ -191,6 +200,19 @@ final class LocationTracker: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Abandonne la trace en cours.
|
||||
///
|
||||
/// Seul recours quand aucun workout n'a pu être associé : sans `discard()`,
|
||||
/// le builder garde ses données côté HealthKit et « any further calls to
|
||||
/// the builder raise an exception ». La sortie existera alors sans
|
||||
/// parcours — perte réelle, à tracer comme telle plutôt qu'à masquer.
|
||||
func discardRoute() {
|
||||
guard let builder = routeBuilder else { return }
|
||||
self.routeBuilder = nil
|
||||
builder.discard()
|
||||
locationLog.error("trace abandonnee : aucun workout a associer (\(self.filter.acceptedCount) points perdus)")
|
||||
}
|
||||
|
||||
// MARK: Écriture HealthKit
|
||||
|
||||
/// Écrit les points en attente. Par lots : une écriture par position
|
||||
|
||||
Reference in New Issue
Block a user