Les Live Activities sont une machine à états, pas un badge
La Live Activity dans Return ressemble à un nombre qui décompte sur l’écran verrouillé et sur la Dynamic Island.12 Ce n’est pas un nombre. C’est une machine à cinq états de cycle de vie avec trois chemins de fermeture externes et un chemin de démarrage réentrant qui doit se défendre contre lui-même. Les patterns ci-dessous sont ceux qui ont survécu à la production. Le pied de page de brutale honnêteté à la fin dit ce que je ne sais pas encore.
J’ai livré une v1 qui traitait la Live Activity comme un badge. Le « temps restant actuel » était la donnée ; le reste était de la décoration. Cette version avait trois bugs que j’ai attrapés en TestFlight et un que j’ai attrapé en production :
- Toucher démarrer alors que le démarrage était déjà en vol créait une seconde activity qui orphelinait la première.
- Le compte à rebours s’affichait correctement sur la Dynamic Island, mais la vue de l’écran verrouillé tombait sur
endTime <= Date()pour les minuteurs en pause et affichait0:00jusqu’à ce que l’utilisateur reprenne. - La Live Activity restait visible longtemps après que l’utilisateur ait réinitialisé le minuteur parce que la politique de fermeture était
.default, qu’Apple maintient visible pendant un certain temps, jusqu’à quatre heures. - (Production.) Sur les locales de langues écrites de droite à gauche (arabe, hébreu), les chiffres s’affichaient à l’envers dans la région compact-trailing de la Dynamic Island. Chiffres latins, mise en page RTL. Le correctif tenait en une ligne.
Chacun de ces problèmes était un bug de machine à états. Le nombre du compte à rebours allait bien. Le nombre n’est pas le produit. Le produit, c’est l’état.
La machine à états ci-dessous est ce qui a survécu à ces bugs.
TL;DR
- Le
LiveActivityManagerlivré expose 5 méthodes de transition (startActivity,updateActivity,showCycleComplete,showFinalCompletion,endActivity) plus 1 lecture (hasActiveActivity). Les 224 lignes de production protègent contre un risque spécifique à l’intérieur destartActivity: les appels de démarrage concurrents plus les vérifications d’annulation à travers chaque frontièreawaitdans cette méthode.3 - Le
ContentStatetransporte 6 champs :endTime,currentCycle,totalCycles,isPaused,isCompleted,remainingSeconds. Les cinq premiers sont les étiquettes de la machine à états. Le sixième (remainingSeconds) est une solution de repli pour l’affichage statique que letimerIntervalen direct d’ActivityKit ne peut pas servir. - La décision de la politique de fermeture est le véritable choix produit.
.immediatepour la réinitialisation utilisateur,.after(Date().addingTimeInterval(3))pour la complétion, jamais le défaut système. - La région compact-trailing de la Dynamic Island a besoin de
.environment(\.layoutDirection, .leftToRight)sur le texte du minuteur pour conserver les chiffres latins LTR sous les locales système RTL.
La machine à états
La Live Activity livrée a un état d’inactivité, trois états en direct que l’utilisateur peut observer, un état terminal et une porte réentrante que le développeur doit observer :
┌──────────────────────────────────────────────────────────────────┐
│ Lifecycle states │
├──────────────────────────────────────────────────────────────────┤
│ IDLE currentActivity == nil; no Live Activity present │
│ RUNNING isPaused=false, endTime > Date() │
│ PAUSED isPaused=true, remainingSeconds=N │
│ CYCLE_END isPaused=false, endTime <= Date(), isCompleted=false│
│ COMPLETE isCompleted=true (terminal; transitions to IDLE) │
└──────────────────────────────────────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────────────┐
│ Dismissal policies (Apple) │
├──────────────────────────────────────────────────────────────────┤
│ .immediate user reset │
│ .after(now + 3s) completion display window │
│ .default system decides; can stay up to 4 hours │
└──────────────────────────────────────────────────────────────────┘
Reentrancy gate inside startActivity():
isStartingActivity flag + cancellable startActivityTask
prevents two concurrent startActivity() calls from creating
two Live Activities for one timer. Cancellation checks across
each await keep the in-flight task safe to abort.
Le chemin de rendu vérifie d’abord isPaused ; cet ordre est ce qui empêche un minuteur en pause de s’afficher comme CYCLE_END lorsque l’horloge murale a dépassé endTime.7
Les noms d’états ne sont pas des étiquettes sur le nombre. Les noms d’états sont le contrat entre LiveActivityManager (côté app, où vivent mes vues SwiftUI) et ReturnLiveActivity (l’extension widget, où le processus d’Apple effectue le rendu de la surface).
Le contrat est TimerActivityAttributes.ContentState, l’ensemble des 6 champs :3
public struct ContentState: Codable, Hashable {
var endTime: Date
var currentCycle: Int
var totalCycles: Int?
var isPaused: Bool
var isCompleted: Bool = false
var remainingSeconds: Int = 0
}
Chaque transition d’état mute cette struct et demande à ActivityKit de la livrer à travers les frontières de processus jusqu’à l’extension widget. Le widget effectue ensuite un nouveau rendu. Il n’y a pas de mémoire partagée. Il n’y a pas de callback. Il y a une struct Codable qui traverse une frontière de processus à chaque transition.
Ce fait exclut tout ce que je pourrais vouloir faire avec des closures, des view models, des observable objects ou des computed properties. L’état doit être exprimable sous forme de données sérialisables. S’il ne peut pas être encodé, il ne peut pas faire de transition.
Le démarrage réentrant
Les Live Activities ont une limite stricte sur les activities concurrentes et une limite souple sur ce qui se passe si vous appelez Activity.request deux fois en vol. La limite stricte est bien documentée.4 La limite souple est : « le second appel peut réussir et créer un orphelin ». L’orphelin est la Live Activity qui n’est plus associée à currentActivity dans votre manager. Elle survit. Elle n’a aucun chemin de retour vers votre code. Elle se ferme d’elle-même sur son minuteur de péremption éventuellement. L’utilisateur voit un minuteur en double jusqu’à ce moment-là.
L’orphelin était le bug v1 que Return a livré. Le correctif est la porte réentrante plus une Task annulable dans LiveActivityManager.swift :3
private var isStartingActivity = false
private var startActivityTask: Task<Void, Never>?
func startActivity(...) {
#if os(iOS)
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
guard !isStartingActivity else { return }
isStartingActivity = true
startActivityTask?.cancel()
startActivityTask = Task {
defer {
isStartingActivity = false
startActivityTask = nil
}
guard !Task.isCancelled else { return }
await endActivity() // explicit cleanup of any prior state
guard !Task.isCancelled else { return }
// ... build attributes + contentState ...
do {
let activity = try Activity.request(...)
guard !Task.isCancelled else { return }
currentActivity = activity
} catch {
// log; flag clears via defer
}
}
#endif
}
Trois choses à propos de ce pattern que la documentation ne mentionne pas :
Le drapeau isStartingActivity est la protection active ; startActivityTask?.cancel() est un nettoyage défensif. Le drapeau court-circuite tout second appel à startActivity pendant que le premier est en vol, donc vous n’entrez pas vraiment en course sur le chemin public. La danse cancel-then-replace compte tout de même parce que la Task en vol est asynchrone et peut survivre à un appelant à courte durée de vie ; l’annulation empêche une Task obsolète de continuer après que l’appelant soit passé à autre chose.
Les vérifications guard !Task.isCancelled à travers chaque frontière await. L’annulation est coopérative en Swift. Même si cancel est appelé, la Task continue de tourner jusqu’à ce qu’elle vérifie explicitement. Chaque await est une opportunité de vérifier. Sans les vérifications post-await, une Task annulée continue à construire l’état de l’activity, appelle Activity.request et crée silencieusement un orphelin en cas de succès.
Le defer efface le drapeau avant que le corps de la Task ne se termine. Sans defer, un return précoce (depuis la vérification d’annulation) laisse isStartingActivity = true de façon permanente et l’activity ne redémarre jamais jusqu’au relancement de l’app. Le drapeau est un verrou ; le verrou doit se relâcher sur chaque chemin de sortie.
L’argument pushType: nil. Return n’utilise pas les mises à jour de Live Activity poussées via APNs. L’app met à jour l’activity localement via activity.update. Si vous avez besoin de mises à jour pilotées par push (suivi de livraison, scores sportifs, données en temps réel), le type est pushType: .token et le contrat est dramatiquement plus complexe.5 Les mises à jour locales sont plus simples et elles couvrent tout flux de minuteur / compteur / app unique.
Le problème de la pause
ActivityKit livre une superbe vue Text(timerInterval: Date()...endTime, countsDown: true) qui affiche un compte à rebours en direct sans aucune mise à jour de l’app.6 Vous définissez l’heure de fin, le système affiche un minuteur en direct. Pas de Timer.publish, pas de rafraîchissement de widget, pas de drain de batterie.
C’est fantastique quand le minuteur tourne. C’est faux quand le minuteur est en pause.
Le texte timerInterval compte vers endTime indépendamment de tout signal de « pause » dans l’état. Il n’y a pas de mode « gelé à 10:23 » dans l’API d’Apple. Si vous passez endTime = Date().addingTimeInterval(623) et que l’utilisateur met en pause à la marque des 10:23, le texte du minuteur continue à décompter jusqu’à zéro dans le widget. Le champ d’état dit pause. Le widget affiche en cours.
Le correctif consiste à effectuer le rendu de deux vues différentes à partir du même état :7
if context.state.isPaused {
// static text
Text(formatTime(context.state.remainingSeconds))
.monospacedDigit()
} else if context.state.endTime > Date() {
// live countdown
Text(timerInterval: Date()...context.state.endTime, countsDown: true)
.monospacedDigit()
} else {
// post-end static
Text("0:00")
.monospacedDigit()
}
Le rendu à deux pistes est la raison pour laquelle le ContentState transporte remainingSeconds comme champ séparé. C’est redondant quand le minuteur tourne (le système le calcule à partir de endTime). C’est la seule source de vérité quand le minuteur est en pause. Les deux moitiés de la struct servent deux modes de rendu différents ; le booléen isPaused choisit entre eux.
Les politiques de fermeture
activity.end(_:dismissalPolicy:) prend l’une des trois valeurs ActivityUIDismissalPolicy, et choisir mal est ce qui a fait que ma v1 est restée sur l’écran verrouillé de l’utilisateur pendant ce qui ressemblait à une éternité après une réinitialisation :13
| Politique | Quand l’utiliser | Ce que vous obtenez |
|---|---|---|
.immediate |
Réinitialisation utilisateur, erreur, app en arrière-plan sans activity à suivre | L’activity disparaît maintenant. Pas de fenêtre de grâce |
.after(date) |
Affichage de complétion : « votre méditation est terminée » doit être lisible un instant. La date doit être dans la fenêtre de quatre heures qu’Apple autorise | L’activity affiche l’état final, puis se ferme à date |
.default |
Quand vous voulez vraiment que les heuristiques d’Apple décident | Le système la garde visible « pendant un certain temps » (formulation d’Apple), jusqu’à quatre heures après l’appel d’end |
Return utilise .after(Date().addingTimeInterval(3)) pour le chemin de complétion naturelle :3
await activity.end(
.init(state: contentState, staleDate: nil),
dismissalPolicy: .after(Date().addingTimeInterval(3))
)
Trois secondes correspondent au temps dont un utilisateur a besoin pour jeter un œil à l’écran verrouillé, enregistrer que le minuteur s’est terminé et ressentir la satisfaction de la coche. Moins de trois est saccadé. Plus de trois donne l’impression que l’activity ne sait pas qu’elle est terminée.
Pour une réinitialisation déclenchée par l’utilisateur, l’appel est dismissalPolicy: .immediate. Pas de fenêtre. L’utilisateur le sait déjà.
Le mauvais choix dans la v1 était .default. Pour un minuteur de méditation terminé, le système gardait l’activity visible suffisamment longtemps pour que les utilisateurs pensent que l’app n’avait pas du tout enregistré la complétion. La documentation d’Apple indique que .default garde l’activity terminée « visible pendant un certain temps » jusqu’à quatre heures ;13 la posture correcte pour un minuteur est de rendre la fermeture explicite.
La région compact de la Dynamic Island
La Dynamic Island a trois modes de rendu et vous avez besoin des trois, même pour un simple minuteur :2
- Compact (forme par défaut de la Dynamic Island) : icône leading + minuteur trailing
- Minimal (lorsqu’une autre Live Activity rivalise pour la même Dynamic Island) : icône leading uniquement
- Expanded (appui long) : quatre régions nommées (
leading,trailing,center,bottom)
Le pattern qui a gagné sa place dans Return consiste à rendre la vue expanded presque identique à compact :8
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image("AppIconSmall")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
DynamicIslandExpandedRegion(.trailing) {
TimerText(...)
}
DynamicIslandExpandedRegion(.center) { EmptyView() }
DynamicIslandExpandedRegion(.bottom) { EmptyView() }
} compactLeading: {
Image("AppIconSmall")...
} compactTrailing: {
TimerText(...)
} minimal: {
Image("AppIconSmall")...
}
La plupart des tutoriels sur les Live Activities s’appuient sur la vue expanded comme étant le « vrai » design, avec un contenu riche dans la région bottom. Pour un minuteur de méditation, l’expansion est un poids mort. L’utilisateur ouvre la vue expanded en appuyant longuement, et l’appui long lui donne déjà le retour haptique indiquant que quelque chose s’est passé. Ajouter du contenu fait que l’expansion dit quelque chose que l’utilisateur n’a pas demandé. Les régions vides en mode expanded ne sont pas un échec du design ; elles sont le design.
Le bug RTL
Le bug de production. Les utilisateurs arabes et hébreux sur iOS ont signalé que le minuteur compact-trailing de la Dynamic Island affichait les chiffres à l’envers. La chaîne de chiffres latins 5:23 s’affichait comme 32:5 parce que la direction de mise en page compact-trailing héritait du paramètre RTL de la locale système.
SwiftUI hérite de la direction de mise en page système à l’intérieur du processus widget, donc le texte du minuteur de la Dynamic Island captait le RTL lorsque le téléphone de l’utilisateur était paramétré en arabe ou en hébreu. Les chiffres latins devraient s’afficher LTR même à l’intérieur d’une UI par ailleurs RTL. Le correctif consiste à épingler la direction de mise en page sur les vues de texte numérique :7
.environment(\.layoutDirection, .leftToRight)
L’override va sur les vues Text numériques à l’intérieur de TimerText (Dynamic Island compact / expanded) et à l’intérieur de la vue de l’écran verrouillé, pas sur la vue entière. Les chiffres latins se lisent de gauche à droite quelle que soit la locale système de l’utilisateur ; les libellés de cycle comme « Cycle 2 sur 3 » restent localisés afin qu’ils suivent la direction de mise en page système.
Le bug n’apparaît pas dans le TestFlight de la locale domestique. Il apparaît au moment où un véritable utilisateur RTL ouvre le minuteur. La leçon : livrer l’override d’environnement épinglé LTR sur chaque vue de texte à chiffres latins dans toute Live Activity susceptible de tourner dans des locales RTL.
L’histoire de la localisation
TimerActivityAttributes transporte un champ languageCode: String défini par l’app à la création de l’activity :9
let attributes = TimerActivityAttributes(
timerDuration: duration,
languageCode: settings.appLanguage // app's selected language, not system's
)
L’extension widget lit cela pour effectuer le rendu des chaînes localisées :
private var locale: Locale {
let code = context.attributes.languageCode
return code.isEmpty ? .current : Locale(identifier: code)
}
private func localized(_ key: String.LocalizationValue) -> String {
String(localized: key, locale: locale)
}
Pourquoi l’app passe son propre code de langue plutôt que de laisser le widget lire Locale.current : l’extension widget tourne dans son propre processus. Son Locale.current est la locale système, pas la locale sélectionnée de l’app. Si un utilisateur a paramétré Return en coréen alors que son iPhone est en anglais, le widget parlerait anglais sans cet override. La préférence de langue de l’app voyage dans les attributes de l’activity ; le widget les honore.
Localizable.xcstrings vit dans la cible widget aux côtés de celle de l’app, mais ce sont des fichiers séparés. Les chaînes utilisées dans le widget doivent exister dans ReturnWidgets/Localizable.xcstrings même si la même chaîne existe dans Return/Localizable.xcstrings. Oublier cela signifie que le widget retombe sur la langue de développement tandis que l’app parle coréen.
Ce que je construirais différemment
Rendre ContentState plus petit. Six champs, c’est trop. La redondance entre endTime et remainingSeconds est le prix à payer pour contourner l’absence de mode pause dans timerInterval. Si je recommençais, je transporterais une seule enum displayMode (running, paused(remainingSeconds: Int), cycleEnd, complete) et laisserais le code de rendu dispatcher sur le cas. Six champs sont plus difficiles à garder correctement mutés à travers cinq méthodes de transition que ne le sont quatre cas.
Ajouter des boutons interactifs Live Activity (iOS 17+). Return n’expose pas actuellement de contrôles pause/reprise dans la Dynamic Island. L’utilisateur doit ouvrir l’app pour mettre en pause. iOS 17 a ajouté Button(intent:) pour les App Intents à l’intérieur des Live Activities.10 Un contrôle de pause interactif est l’extension évidente et la prochaine chose que je livrerai pour Return.
Live Activities à mise à jour push pour la synchronisation de minuteur entre appareils. Return synchronise les sessions entre iPhone, iPad, Watch et Apple TV via NSUbiquitousKeyValueStore (couvert dans Cinq plateformes Apple, trois fichiers partagés). Aujourd’hui, l’activity est démarrée localement depuis l’app iPhone ou iPad et mise à jour localement. Un utilisateur démarrant un minuteur sur Apple Watch pourrait idéalement voir la Live Activity refléter cela sur l’iPhone en temps réel. Le push APNs vers la Live Activity est le chemin.5 Pas encore construit.
Quand ne pas utiliser les Live Activities
État transitoire à coup unique. Un toast « enregistré ! » ne mérite pas une Live Activity. Le système a une bannière. Utilisez-la.
Données changeant fréquemment sans dimension de minuteur. Les Live Activities fonctionnent mieux pour les choses ayant un ancrage temporel clair (un minuteur, un ETA de livraison, une horloge de jeu, une durée d’appel téléphonique). Les tickers boursiers et les scores sportifs fonctionnent parce qu’ils ont une fenêtre de session. Un dashboard à usage général ne fonctionne pas.
Apps sans cas d’usage écran verrouillé / standby. Les Live Activities demandent un véritable investissement d’ingénierie (configuration de cible, design du ContentState, décisions de politique de fermeture, gestion RTL, plomberie de localisation). Les apps que l’utilisateur ouvre directement sans jamais consulter l’écran verrouillé pendant l’usage ne sont pas la bonne forme. Un éditeur de photos n’en a pas besoin. Un tracker d’entraînement, oui.
Sur les surfaces non-iOS, avec des réserves. Le LiveActivityManager de Return livre son implémentation derrière #if os(iOS) parce que le minuteur est démarré depuis l’app iPhone ou iPad. ActivityKit lui-même décrit la bannière de l’écran verrouillé, la Dynamic Island, le Smart Stack d’Apple Watch, le Mac et CarPlay comme des surfaces de présentation ; iOS 26 a étendu plusieurs d’entre elles.4 watchOS a toujours ses propres complications API pour le rendu plein écran. macOS a des apps de barre de menus. iPadOS supporte les Live Activities depuis iPadOS 17 sans région Dynamic Island. Le manager de Return a 8 gardes #if os(iOS) répartis sur un seul fichier de 224 lignes.
Ce que le pattern signifie pour les apps livrant sur iOS 26+
Deux enseignements.
-
Traitez la Live Activity comme une machine à états, pas comme un nombre. La machine à états a des états clairs, des transitions claires et des règles de fermeture claires. Le nombre à l’écran est un rendu d’un état. Mettez d’abord les états au point.
-
La garde de réentrance est le bug que vous n’avez pas encore touché. Chaque manager de Live Activity que j’ai vu en production qui n’implémente pas
isStartingActivity+ Task annulable a livré au moins un bug d’activity orpheline. La garde fait 6 lignes. Écrivez-la une fois.
Associez ce billet à mes écrits précédents sur la même famille d’apps : les App Intents typés pour Apple Intelligence ; les serveurs MCP pour les agents inter-LLM ; les patterns Liquid Glass pour la couche visuelle ; la livraison multiplateforme pour la portée inter-appareils. Les Live Activities sont la couche écran-verrouillé-iOS-et-Dynamic-Island de la même pile. L’ensemble complet vit au hub de la série Écosystème Apple. Pour le contexte plus large iOS-avec-agents-IA, consultez le guide de développement d’agents iOS.
FAQ
Quelle est la différence entre les Live Activities et les widgets WidgetKit ?
Les widgets WidgetKit effectuent un rendu à des intervalles définis par TimelineProvider ; le système décide quand rafraîchir et le widget effectue un nouveau rendu à partir d’une timeline statique.11 Les Live Activities effectuent un rendu en réponse à des appels activity.update(...) spécifiques pilotés par l’app et vivent pour la durée de l’activity sous-jacente (un minuteur, une livraison, un entraînement). Les deux sont livrés dans la cible d’extension widget ; la différence est le modèle de déclenchement.
Les Live Activities fonctionnent-elles sur iPad ?
Oui, dans iPadOS 17+. La bannière de l’écran verrouillé est la surface de rendu principale ; l’iPad n’a pas de Dynamic Island. Le même code ActivityConfiguration fonctionne ; attendez-vous simplement à ce que les régions de la Dynamic Island ne s’affichent jamais sur iPad.
Une Live Activity peut-elle survivre à mon processus app ?
Oui. Une fois Activity.request réussi, ActivityKit possède l’activity. Le processus app peut être terminé par le système ; l’activity continue à effectuer son rendu sur l’écran verrouillé et la Dynamic Island jusqu’à ce que vous la terminiez explicitement (ou jusqu’à ce que les règles de péremption du système la ferment). Les appels explicites endActivity() comptent pour cette raison ; sans une fin explicite à la réinitialisation de l’app, l’activity survit au minuteur.
Pourquoi le billet ne couvre-t-il pas les Live Activities mises à jour par push ?
Je n’ai pas livré de Live Activities mises à jour par push dans Return. Selon la règle de genre pour ce cluster : les billets de code livré documentent uniquement ce que fait le code de production. Les mises à jour push sont listées dans « Ce que je construirais différemment » ; un futur billet les couvrira après que je les aurai livrées.
Quelle est la disposition réelle des fichiers pour les Live Activities dans une app SwiftUI ?
- Dans la cible app principale :
LiveActivityManager.swift(gère le cycle de vie de l’activity),TimerActivityAttributes.swift(la structActivityAttributespartagée avec le widget ; les deux cibles compilent ce fichier). - Dans une cible d’extension widget :
ReturnLiveActivity.swift(la conformanceWidgetavec le corpsActivityConfiguration),ReturnWidgetsBundle.swift(le@main WidgetBundle). - Configuration :
Info.plistavecNSSupportsLiveActivities = YESdans la cible app.
La cible d’extension widget a besoin des imports ActivityKit et WidgetKit. TimerActivityAttributes est le seul fichier partagé entre les deux cibles ; tout le reste est isolé par cible.
La Live Activity n’est pas un nombre sur l’écran verrouillé. C’est une machine à états qui traverse une frontière de processus à chaque transition. Mettez les états au point, gardez la réentrance, choisissez la politique de fermeture intentionnellement et épinglez la direction de mise en page. Le nombre se débrouille tout seul.
Références
-
L’auteur a livré Return, 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. Les Live Activities sont livrées sur la cible iOS uniquement. ↩
-
Apple Developer, « ActivityKit framework ». Bannière de l’écran verrouillé, modes Dynamic Island compact / minimal / expanded, cycle de vie de l’activity. Disponible sur iOS 16.1+ ; Dynamic Island disponible sur iPhone 14 Pro et ultérieurs. ↩↩
-
Code de production dans
Return/Return/LiveActivityManager.swift(224 lignes, 8 blocs#if os(iOS)) etReturn/Return/TimerActivityAttributes.swift(43 lignes). Partagé entre la cible app et la cible d’extension widget via l’appartenance à la cible. ↩↩↩↩↩ -
Apple Developer, « Displaying live data with Live Activities ». Limites de concurrence, plateformes supportées (iOS 16.1+, iPadOS 17+), clé Info.plist
NSSupportsLiveActivities. ↩↩ -
Apple Developer, « Updating and ending your Live Activity with ActivityKit push notifications ». Le chemin
pushType: .tokenrequiert une clé d’auth APNs séparée, l’enregistrement de jetons push côté serveur et un protocole de mise à jour différent des appels locauxactivity.update(...). ↩↩ -
Apple Developer, « Text(timerInterval:pauseTime:countsDown:showsHours:) ». Minuteur à compte à rebours rendu en direct par le système ; effectue son rendu sans mises à jour de l’app pendant que l’activity tourne. ↩
-
Code de production dans
Return/ReturnWidgets/ReturnLiveActivity.swift(232 lignes). La conformanceWidgetde l’extension widget avec le corpsActivityConfiguration<TimerActivityAttributes>. La vueTimerTextaux lignes 61-102 gère le rendu à trois états en pause / en cours / post-fin. ↩↩↩↩ -
Apple Developer, « DynamicIsland ». Les quatre régions expanded nommées (
leading,trailing,center,bottom) plus trois vues en mode compact (compactLeading,compactTrailing,minimal). ↩ -
L’extension widget tourne dans son propre processus et hérite de la locale système, pas de la locale sélectionnée de l’app. Les apps qui supportent le changement de langue dans l’app (Return supporte 27 langues) doivent passer le code de langue à travers
ActivityAttributesafin que le widget puisse effectuer son rendu dans la langue choisie de l’utilisateur. Pattern :Locale(identifier: context.attributes.languageCode)plutôt queLocale.current. ↩ -
Apple Developer, « Button(intent:) ». Disponible dans les vues widget et Live Activity à partir d’iOS 17+. Fait le pont entre les App Intents et les contrôles de l’écran verrouillé / Dynamic Island sans nécessiter le passage au premier plan de l’app. ↩
-
Apple Developer, « TimelineProvider ». Le modèle de rafraîchissement de widget qui précède les Live Activities ; entrées pré-calculées avec fenêtres de rechargement gérées par le système. ↩
-
Code de production dans
Return/ReturnWidgets/ReturnWidgetsBundle.swift(16 lignes). Le@main WidgetBundlequi enregistreReturnLiveActivitycomme seul widget de l’extension widget. Pattern requis pour les extensions widget ; le bundle est ce que le système charge. ↩ -
Apple Developer, « ActivityUIDismissalPolicy ». Trois cas :
.default,.immediate,.after(_:). Apple indique que.defaultgarde une Live Activity terminée visible « pendant un certain temps » jusqu’à quatre heures, et.after(_:)accepte une date dans la même fenêtre de quatre heures. ↩↩