Le Runtime watchOS Est un Contrat, Pas une Tâche d'Arrière-Plan
Genre : shipped-code. Cet article documente le pattern de runtime watchOS que Return livre en production. Return est le minuteur de méditation SwiftUI sur l’App Store ; l’application Watch exécute un minuteur multi-cycles qui doit continuer à compter lorsque l’utilisateur baisse le poignet.1 Le pattern qui survit à cette contrainte est WKExtendedRuntimeSession plus un délégué global au périmètre de l’application. Tout le reste meurt à l’instant où la montre s’endort.
watchOS n’est pas iOS avec un écran plus petit. Le modèle de runtime est différent. iOS donne à une application un budget de premier plan généreux et un runtime d’arrière-plan qui s’amenuise mais reste réel grâce aux sessions audio, aux mises à jour de localisation, à BGTaskScheduler, et à une poignée d’autres facilités.2 watchOS donne à l’application au premier plan un budget mesuré en secondes après que le poignet retombe, et après cela, l’application est suspendue à moins qu’elle n’ait signé un contrat de runtime avec le système. Il n’y a pas de facilité « je fais juste un truc en arrière-plan ». Il y a « j’exécute une séance d’entraînement, une session de pleine conscience, une alarme intelligente, un itinéraire de navigation, ou une tâche de surveillance de la santé », et rien d’autre.3
La cible Watch de Return est un minuteur de pleine conscience. Le contrat de session est WKBackgroundModes: mindfulness. La API de runtime est WKExtendedRuntimeSession. Le pattern qui a fait passer l’application Watch de cassée lorsque le poignet retombe à survit à une méditation de 25 minutes est celui que cet article décrit.
TL;DR
- watchOS n’a pas d’arrière-plan à la manière d’iOS. Le runtime de premier plan se termine peu après la baisse du poignet, et seuls les types de sessions enregistrées continuent à s’exécuter.
WKExtendedRuntimeSessionest la surface de la API. La session doit déclarer un type de session ; pour un minuteur de méditation, le type est implicite viaWKBackgroundModes: mindfulnessdansInfo.plist.- Le gestionnaire de session doit vivre au périmètre de l’application, pas au périmètre de la vue. Le cycle de vie des vues SwiftUI désalloue les objets détenus par la vue lors de la navigation ; un délégué de session désalloué est une session morte, même si la session elle-même est toujours en cours d’exécution.
- Les callbacks
WKExtendedRuntimeSessionDelegateconstituent le contrat :didStart,willExpire,didInvalidateWith. Le callback d’expiration se déclenche avant que le système ne force l’invalidation ; Apple le décrit comme la fenêtre permettant à l’application de « terminer et nettoyer ». - Une baisse du poignet sans session étendue active met le minuteur en pause. Une baisse du poignet avec une session étendue active fait continuer le minuteur. La session est la différence entre « produit livré » et « cassé à la deuxième utilisation ».
Le Problème de l’Arrière-Plan que watchOS Ne Résout Pas à la Manière iOS
Les applications iOS recourent à plusieurs facilités d’arrière-plan lorsqu’elles ont besoin que l’application continue à s’exécuter avec l’écran éteint :2
- Une
AVAudioSessionavec la catégorie.playbackmaintient une application audio en vie pendant la lecture de la musique. - Les mises à jour d’arrière-plan de
CLLocationManagermaintiennent une application de navigation en vie avec une barre bleue. BGTaskSchedulermet en file d’attente du travail de maintenance court que le système planifie selon sa propre horloge.- Une extension d’interface utilisateur de premier plan (Live Activity, CallKit, PushKit) relie le processus de l’application à une surface de rendu contrôlée par le système.
Aucune de ces options n’aide sur watchOS de la manière dont vous pourriez le supposer. Les applications Watch n’ont pas le même planificateur de tâches d’arrière-plan. Elles n’ont pas de mode AVAudioSession.playback en arrière-plan qui maintient le minuteur en train de compter en silence. Elles ont une primitive structurelle pour « je veux continuer à m’exécuter après que l’utilisateur a baissé le poignet », et cette primitive est WKExtendedRuntimeSession avec un type de session déclaré.3
Les types de sessions qu’Apple prend en charge via WKBackgroundModes sont volontairement restreints :4
workout-processing(avecHKWorkoutSessionpour de véritables séances d’entraînement)mindfulness(pour les minuteurs de méditation et les exercices de respiration)self-care(pour les routines guidées)physical-therapy(pour les applications de séances de thérapie)alarm(pour les alarmes de réveil basées sur le temps)underwater-depth(pour les applications de plongée et de suivi de profondeur)
Les applications qui ne rentrent pas dans l’une de ces catégories ne peuvent pas utiliser WKExtendedRuntimeSession pour s’exécuter après la baisse du poignet. Les applications audio recourent à la catégorie de session audio mediaPlayback et à l’intégration Now Playing sur un chemin de code différent ; les applications de navigation utilisent les mises à jour d’arrière-plan de CLLocationManager. La Watch n’est pas un ordinateur à usage général ; c’est un appareil avec des contraintes de batterie que le modèle de runtime fait respecter.
Un minuteur de méditation rentre dans mindfulness. Le contrat : déclarez le mode d’arrière-plan dans Info.plist, demandez une WKExtendedRuntimeSession, gérez les callbacks du délégué, terminez la session lorsque le minuteur se termine. Le système accorde jusqu’à environ une heure de runtime par session, avec sa propre discrétion pour raccourcir cette durée sous pression thermique ou de batterie.3
Le Pattern que Return Livre
Le pattern commence par la déclaration Info.plist :4
<key>WKBackgroundModes</key>
<array>
<string>mindfulness</string>
</array>
La déclaration du mode est ce qui rend le type de session valide. Sans elle, l’appel à WKExtendedRuntimeSession().start() échoue silencieusement et l’application est suspendue lors de la baisse du poignet exactement comme une application Watch sans aucun mode d’arrière-plan.
Le gestionnaire de session lui-même doit vivre au périmètre de l’application. Le cycle de vie des vues SwiftUI est hostile aux objets avec état à longue durée de vie : @StateObject et @State sont délimités par la vue qui les possède, et un push de navigation qui remplace la vue fait disparaître l’état avec elle. Une WKExtendedRuntimeSession dont le délégué est désalloué en pleine session ne plante pas ; la session continue à s’exécuter, mais les callbacks du délégué (willExpire, didInvalidateWith) atteignent un objet libéré, ce qui signifie que le nettoyage n’a jamais lieu, ce qui signifie que le prochain appel startSession() pense qu’il n’y a pas de session active et démarre un doublon.
Le pattern livré est un singleton au périmètre de l’application. L’extrait ci-dessous est la forme structurelle ; en production, on ajoute du logging à l’intérieur de chaque méthode pour l’observabilité :
import SwiftUI
import WatchKit
final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate {
static let shared = WatchSessionManager()
private var session: WKExtendedRuntimeSession?
private override init() {
super.init()
}
var isSessionActive: Bool {
session != nil
}
func startSession() {
guard session == nil else { return }
let newSession = WKExtendedRuntimeSession()
newSession.delegate = self
newSession.start()
session = newSession
}
func endSession() {
guard let existing = session else { return }
existing.invalidate()
session = nil
}
// MARK: - WKExtendedRuntimeSessionDelegate
func extendedRuntimeSessionDidStart(_ session: WKExtendedRuntimeSession) {}
func extendedRuntimeSessionWillExpire(_ session: WKExtendedRuntimeSession) {
// Apple's "about to expire / finish and clean up" hook
}
func extendedRuntimeSession(
_ session: WKExtendedRuntimeSession,
didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
error: Error?
) {
self.session = nil
}
}
Trois détails à propos de ce singleton que la documentation ne mentionne pas :
Le static let shared plus @State private var sessionManager = WatchSessionManager.shared au niveau @main App maintient le gestionnaire en vie pour toute la durée du processus de l’application Watch. SwiftUI ne retient pas les singletons simplement parce qu’une vue les détient ; le binding ci-dessus est ce qui indique au runtime de conserver la référence. Sans le binding au niveau App, ARC peut faire disparaître le gestionnaire lorsqu’aucune vue ne le détient.
La propriété session est la protection contre les sessions en double. Un minuteur avec un bouton « recommencer » peut appeler startSession() depuis plusieurs chemins ; la vérification guard session == nil est le verrou. Deux sessions étendues concurrentes provoquent un comportement imprévisible : parfois la seconde réussit et la première devient orpheline, parfois l’appel de démarrage échoue silencieusement. L’invariant de session unique empêche toute la classe de problèmes.
Les callbacks du délégué loggent mais agissent rarement. Le callback didStart se déclenche une fois par session et est un point d’accroche utile pour l’observabilité ; le callback willExpire se déclenche avant que le système ne force l’invalidation et c’est là qu’Apple s’attend à ce que l’application « termine et nettoie » ; le callback didInvalidateWith est l’endroit où la référence de session est effacée afin que le prochain appel startSession() fonctionne. Le pattern en production est les callbacks mettent à jour l’état, la machine à états fait le travail, pas les callbacks font le travail directement.
Le gestionnaire de minuteur appelle le gestionnaire de session à chaque transition qui modifie l’état de comptage actif du minuteur :
final class WatchTimerManager: ObservableObject {
func start() {
startExtendedSession() // -> WatchSessionManager.shared.startSession()
// ... start the timer state machine ...
}
func pause() {
timer?.invalidate()
isRunning = false
endExtendedSession() // -> WatchSessionManager.shared.endSession()
}
func reset() {
// ... clear timer state ...
endExtendedSession()
}
private func completeCycle() {
// ... last cycle handling ...
endExtendedSession() // ends on final completion
}
}
La session se termine sur pause, sur réinitialisation, et à l’achèvement du cycle final. Le raisonnement produit : une méditation en pause n’a pas besoin de continuer à réclamer le budget de runtime que le système accorde sous mindfulness ; reprendre depuis une pause acquiert à nouveau une session fraîche. Le coût produit est qu’une pause poignet-baissé ne peut pas être reprise par la seule levée du poignet ; l’utilisateur doit ramener l’application au premier plan pour reprendre. Le gain produit est que le coût en batterie d’un minuteur en pause tombe à zéro et que le système ne voit pas une session obsolète.
La Baisse du Poignet Est le Test
Tester watchOS dans le simulateur est une fiction polie. Le simulateur ne fait pas respecter le modèle de runtime de baisse du poignet de la manière dont une vraie Apple Watch le fait. Le simulateur garde l’application au premier plan tant que la fenêtre du simulateur a le focus ; une session de runtime étendue dans le simulateur a l’air identique à l’absence de session, parce que l’application au premier plan continue à s’exécuter dans les deux cas.
Le test réel se fait sur une vraie Apple Watch :5
- Lancez le minuteur.
- Baissez le poignet (ou appuyez sur le bouton latéral pour verrouiller l’écran).
- Attendez 30 secondes.
- Relevez le poignet.
Sans une session de runtime étendue active, l’application Watch est suspendue ; l’état du minuteur est figé au moment de la baisse du poignet et reprend à partir de cet état figé. Pour une méditation de 5 minutes durant laquelle l’utilisateur ferme les yeux, le bug est invisible jusqu’à ce que le minuteur soit faux d’autant de temps que les yeux ont été fermés.
Avec une session de runtime étendue active, le minuteur continue à compter. La levée du poignet révèle le minuteur à la position écoulée correcte. Le signal audio (si le minuteur en joue un à la fin) se déclenche à l’heure correcte sur l’horloge murale, et non à l’heure de la levée du poignet.
Le scénario de baisse du poignet était le bug que Return a livré dans la v1 et corrigé dans la v2. Le correctif est le pattern singleton ci-dessus ; le bug était une instance WatchSessionManager détenue par une vue SwiftUI qui était désallouée lors d’un push de navigation. La session s’exécutait techniquement côté système, mais le délégué était libéré ; l’appel suivant de démarrage de session était silencieusement sans effet parce que la propriété session du gestionnaire avait été définie sur un objet désormais mort. Les tests sur appareil réel font apparaître la défaillance en quelques secondes. Les tests sur simulateur ne la font jamais apparaître.
Ce Que Vous Disent Réellement les Callbacks du Délégué
WKExtendedRuntimeSessionInvalidationReason énumère les manières dont une session se termine :6
| Raison | Quand cela se produit |
|---|---|
none |
La session a été explicitement invalidée par l’application appelant invalidate() |
sessionInProgress |
Une session du même type est déjà en cours d’exécution |
expired |
La limite de temps imposée par le système a été atteinte |
resignedFrontmost |
Une autre application est devenue au premier plan pendant l’exécution de la session |
suppressedBySystem |
Le système a supprimé la session (faible alimentation, pression thermique) |
error |
Une erreur irrécupérable s’est produite ; vérifiez le paramètre error |
Les raisons qui comptent pour la conception du produit :
expired signifie que l’utilisateur a obtenu la session complète que vous avez demandée. La session s’est exécutée jusqu’à sa fin naturelle. La durée de méditation la plus longue de Return est de 60 minutes, ce qui est juste à la limite de ce que les sessions mindfulness se voient typiquement accorder. Une méditation de 90 minutes atteindrait régulièrement expired et le minuteur mourrait en pleine session. La décision produit est de plafonner les durées disponibles à ce que le modèle de runtime peut réellement délivrer.
resignedFrontmost signifie que l’utilisateur a ouvert une autre application Watch et que votre session a perdu. Les utilisateurs de la Watch sont doués pour glisser vers une application différente puis oublier. La décision produit est soit pause-on-resign (état préservé, l’utilisateur peut revenir), soit end-on-resign (session terminée, l’utilisateur reçoit un signal « vous vous êtes arrêté tôt »). Return choisit pause-on-resign afin que l’utilisateur puisse prendre un appel téléphonique en pleine méditation et revenir.
suppressedBySystem est la version polie de « la montre est chaude ». Un appareil watchOS sous pression thermique ou avec une faible batterie peut révoquer une session de runtime étendue même sans mauvaise utilisation par l’application. Le gestionnaire de session doit gérer le cas avec élégance : effacer la référence, faire remonter un avertissement non bloquant, et ne pas entrer dans un état où il essaie de redémarrer une session que le système vient de refuser.
Le callback willExpire se déclenche lorsque la session est sur le point d’expirer et est documenté comme le moment où l’application doit « terminer et nettoyer ».3 Le callback est l’endroit où une application peut écrire un instantané d’état final, jouer un signal audio de clôture, ou présenter une interface utilisateur « session bientôt terminée ». Return aujourd’hui ne fait que logger le callback ; un nettoyage plus riche (entrée de journal HealthKit, fondu audio sortant) se produit sur les chemins de réinitialisation et d’achèvement du minuteur et figure sur la liste ce que je construirais différemment pour la fenêtre willExpire.
Ce Que Je Construirais Différemment
Deux choses, si Return repartait de zéro.
Utiliser HKWorkoutSession pour toute session dont la valeur augmente avec l’intégration HealthKit. Un minuteur de méditation se trouve à la frontière entre mindfulness et workout-processing. Mindfulness était le bon choix pour la v1 parce que le modèle de données est plus simple et que l’attente de l’utilisateur est « c’est de la méditation, pas un entraînement ». HKWorkoutSession apporte une intégration HealthKit plus granulaire (début de session, fin de session, segments, événements) et offre une interface LiveWorkoutBuilder plus riche pour accumuler des données. Le jugement architectural, et non une garantie documentée d’Apple : pour une application dont la valeur dépend d’une télémétrie de session détaillée, la voie de la session d’entraînement gère une structure que WKExtendedRuntimeSession ne gère pas.
Ajouter une surface d’observabilité de l’état de session dès le premier jour. La première version de Return loggait les événements de session vers la console. La deuxième version a ajouté la visibilité de l’état de session sur l’appareil pour le débogage. La troisième exposerait un toggle de mode développeur qui ferait remonter l’historique des raisons de session à l’utilisateur lorsque quelque chose ne va pas, au lieu de traiter l’invalidation de session comme une boîte noire. Le runtime watchOS est opaque ; la surface de débogage doit compenser.
Quand WKExtendedRuntimeSession Est la Mauvaise Réponse
Trois cas où le type de session ne convient pas :
Les séances d’entraînement qui nécessitent des marqueurs de segment, des flux de fréquence cardiaque ou un suivi des calories actives. Utilisez HKWorkoutSession directement avec un HKLiveWorkoutBuilder. La API Workout est le chemin documenté par Apple pour de véritables séances d’entraînement (et les méditations en marchant ou les activités intenses) ; WKExtendedRuntimeSession est le chemin documenté pour les sessions sans entraînement comme la pleine conscience ou les alarmes. Une application de méditation n’a pas besoin d’une séance d’entraînement ; une application Couch-to-5K en a besoin.
La lecture audio qui nécessite une surface Now Playing. Utilisez une AVAudioSession configurée pour la lecture parallèlement aux entitlements de session audio watchOS ; l’intégration Now Playing plus la surface de lecture système sont ce que veulent les applications audio, et le chemin audio est entièrement séparé de WKExtendedRuntimeSession. WKExtendedRuntimeSession ne vous donne pas Now Playing ni le routage audio système.
La synchronisation de données à long terme sans la conscience de l’utilisateur. Utilisez WKApplicationRefreshBackgroundTask pour les fenêtres de rafraîchissement périodiques que le système planifie. L’utilisateur n’est pas dans l’application ; l’application n’a pas besoin de continuer à s’exécuter ; elle a besoin de se réveiller brièvement et de se rafraîchir. Les deux modèles tâche-d’arrière-plan et session-de-runtime-étendue répondent à des besoins très différents.
Ce Que le Pattern Signifie pour les Applications Livrées sur watchOS 11+
Trois enseignements à retenir.
-
Le modèle de runtime de la Watch est opt-in. Choisissez un type de session et vivez à l’intérieur de ses règles. Les applications qui essaient de faire du « travail d’arrière-plan général » sur watchOS perdront. Choisissez
mindfulness,workout-processing,self-care,physical-therapy,alarm, ouunderwater-depth, et concevez l’expérience utilisateur autour du budget de runtime qui accompagne le type de session que vous avez choisi. -
Le délégué de session doit vivre au périmètre de l’application. Le cycle de vie des vues SwiftUI ne protège pas les objets avec état à longue durée de vie. Un singleton
static let sharedlié au niveau@main Appest le plus petit pattern qui survit aux push de navigation, aux remplacements de vue, et au comportement normal de désallocation de SwiftUI. -
Testez sur du matériel réel. Le simulateur ne fait pas respecter le modèle de runtime de baisse du poignet. Le bug qu’une application Watch ne peut pas tester dans le simulateur est le bug qu’elle livre aux utilisateurs.
Associez cet article à mes écrits précédents sur la même famille d’applications : la livraison SwiftUI multiplateforme (Return est livré sur iPhone, iPad, Watch, Mac, et Apple TV) ; la machine à états Live Activities (la surface côté iOS pour le même minuteur) ; les patterns HealthKit (où atterrissent les sessions de pleine conscience de la Watch dans les données Santé de l’utilisateur). L’ensemble complet vit sur le hub de la série Apple Ecosystem. Pour un contexte plus large iOS-avec-agents-IA, consultez le guide de développement d’agents iOS.
FAQ
Qu’est-ce qu’une session de runtime étendue watchOS ?
Une session de runtime étendue watchOS (WKExtendedRuntimeSession) est la API qu’une application Watch utilise pour continuer à s’exécuter après que l’utilisateur a baissé le poignet. La session doit déclarer un type (mindfulness, workout-processing, alarm, etc.) via WKBackgroundModes dans Info.plist. Sans une session étendue active, watchOS suspend l’application peu après la baisse du poignet.
Pourquoi mon minuteur watchOS arrête-t-il de compter lorsque l’utilisateur baisse le poignet ?
L’application Watch est suspendue peu après la baisse du poignet à moins qu’une WKExtendedRuntimeSession active d’un type pris en charge ne soit en cours d’exécution. Un gestionnaire de minuteur qui ne démarre pas une telle session verra son runtime d’arrière-plan coupé, et l’état du minuteur se fige au moment de la baisse du poignet jusqu’à ce que l’utilisateur relève le poignet.
Quelle est la différence entre WKExtendedRuntimeSession et HKWorkoutSession ?
WKExtendedRuntimeSession est la API de runtime étendu à usage général pour les sessions sans entraînement comme la pleine conscience, l’alarme, ou le self-care. HKWorkoutSession est la API pour de véritables séances d’entraînement ; elle s’intègre à HealthKit, prend en charge les marqueurs de segment, et est le chemin documenté pour les méditations en marchant ou les activités intenses. Les applications de pleine conscience sans télémétrie de niveau entraînement utilisent la première ; les applications d’entraînement utilisent la seconde.
Le système peut-il révoquer ma session de runtime étendue ?
Oui. WKExtendedRuntimeSessionInvalidationReason inclut expired (limite de temps système atteinte), resignedFrontmost (une autre application Watch est devenue au premier plan), et suppressedBySystem (faible alimentation ou pression thermique). Le gestionnaire de session doit gérer chaque cas proprement : la référence s’efface, l’état du minuteur réagit de manière appropriée, et le prochain appel de démarrage de session fonctionne correctement.
Où le gestionnaire de session doit-il vivre dans une application watchOS SwiftUI ?
Au périmètre de l’application, en tant que singleton lié depuis la struct @main App. L’état au périmètre de la vue SwiftUI (@State, @StateObject) est désalloué lors des push de navigation, des remplacements de vue, ou du passage de l’application en arrière-plan. Un délégué de session détenu par la vue qui est libéré en pleine session provoque une fuite de la référence de session et empêche les sessions suivantes de démarrer proprement.
Références
-
Return de l’auteur, un minuteur de méditation SwiftUI publié sur l’App Store le 21 avril 2026, disponible pour iPhone, iPad, Mac, Apple Watch, et Apple TV. L’application Watch utilise
WKExtendedRuntimeSessionavec le mode d’arrière-planmindfulnesspour le runtime du minuteur de cycles. ↩ -
Apple Developer, « About the background execution sequence ». Facilités de runtime d’arrière-plan côté iOS (sessions audio, localisation, BGTaskScheduler) et en quoi elles diffèrent de watchOS. ↩↩
-
Apple Developer, « WKExtendedRuntimeSession ». Types de sessions, cycle de vie, callbacks du délégué, limites de runtime, et la clé Info.plist
WKBackgroundModes. ↩↩↩↩ -
Apple Developer, « Information Property List: WKBackgroundModes ». Chaînes de types de session prises en charge :
workout-processing,mindfulness,self-care,physical-therapy,alarm,underwater-depth. ↩↩ -
Apple Developer, « Building a watchOS app » et le guide de test WatchKit. Le comportement de runtime sur appareil réel n’est pas reproductible dans le simulateur watchOS ; le simulateur ne fait pas respecter la suspension lors de la baisse du poignet. ↩
-
Apple Developer, « WKExtendedRuntimeSessionInvalidationReason ». Cas d’énumération :
none,sessionInProgress,expired,resignedFrontmost,suppressedBySystem,error. ↩