Live Activities et Dynamic Island avec ActivityKit : le guide complet pour iOS 26

Tout pour créer des Live Activities sur iOS 26 avec ActivityKit : configuration Xcode, ActivityAttributes, Dynamic Island, mises à jour push APNs et nouveautés Apple Watch.

Live Activities iOS 26 : Guide ActivityKit

Mis à jour : 1 juillet 2026

Une Live Activity est une expérience temps réel propulsée par ActivityKit qui affiche les informations en cours de votre application sur l'écran verrouillé, dans la Dynamic Island, et (depuis iOS 26) sur Apple Watch ainsi que dans le StandBy de l'iPad. Concrètement, vous déclarez un ActivityAttributes, démarrez une activité via Activity.request(...), puis poussez des mises à jour locales ou via APNs jusqu'à expiration. Ce guide couvre l'intégralité du pipeline, des nouveautés iOS 26 aux pièges de production que j'ai rencontrés en shippant ma première Live Activity de livraison l'an dernier.

  • Les Live Activities reposent sur ActivityKit et un Widget Extension partagé. Un seul code SwiftUI alimente la Dynamic Island, l'écran verrouillé et désormais la Smart Stack d'Apple Watch.
  • iOS 26 introduit l'ActivityFamily.watch, l'API relevanceScore pour l'ordonnancement automatique, et la résolution des fuites de tokens push avec pushTokenUpdates.
  • Une Live Activity dure 8 heures en premier plan et jusqu'à 12 heures avant suppression forcée par le système. Au-delà, utilisez un staleDate pour signaler des données obsolètes.
  • Les mises à jour push exigent un certificat APNs liveactivity dédié et un payload spécifique avec event: "update" ou "end".
  • La taille du payload est limitée à 4 Ko et la fréquence à environ 1 push toutes les 15 secondes. Au-delà, iOS étrangle silencieusement votre application.

Qu'est-ce qu'une Live Activity ?

Une Live Activity est une mini-interface SwiftUI persistante qui suit un événement en cours (une course Uber, un match, un timer de cuisson, la livraison d'un colis) et qui apparaît simultanément sur l'écran verrouillé, dans la Dynamic Island (iPhone 14 Pro et ultérieurs) et désormais sur la Smart Stack d'Apple Watch en iOS 26. Contrairement aux notifications, une Live Activity n'est pas un événement ponctuel. Elle est démarrée explicitement par l'application, vit jusqu'à 12 heures, et peut être mise à jour à chaud sans réveiller l'application principale.

Du point de vue de l'architecture, ActivityKit alimente un Widget Extension partagé (le même target qui produit vos widgets Home Screen). Vous définissez un ActivityAttributes qui décrit les données statiques du voyage (numéro de commande, adresse de retrait) et un ContentState qui décrit l'état dynamique (étape actuelle, ETA, prix). Le système se charge du rendu, de la persistance et du transport. Votre seule responsabilité est de pousser des états cohérents au bon moment.

Apple positionne explicitement les Live Activities comme l'évolution naturelle des notifications répétées. Si vous spammiez l'utilisateur avec dix push pour la livraison d'un seul Uber Eats, vous devriez aujourd'hui consolider tout cela dans une seule Live Activity. Pour les détails du contrat système, consultez la documentation ActivityKit officielle.

Configurer son projet Xcode 26

Avant d'écrire la moindre ligne d'Activity, trois étapes de configuration sont obligatoires. Oublier l'une d'elles fait silencieusement échouer Activity.request sans message d'erreur exploitable (j'ai perdu une demi-journée sur ce piège lors de mon premier proto).

1. Ajouter la capability Push Notifications au target principal, même si vous ne prévoyez que des mises à jour locales pour l'instant. Apple a unifié le tooling depuis iOS 17.2 et la capacité est obligatoire.

2. Déclarer la clé NSSupportsLiveActivities dans l'Info.plist du target principal :

<key>NSSupportsLiveActivities</key>
<true/>
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>

La seconde clé débloque les mises à jour à haute fréquence (un timer chaque seconde, par exemple) mais désactive le caching agressif de la batterie. Ne l'activez que si l'expérience l'exige réellement.

3. Créer un Widget Extension via File → New → Target → Widget Extension, puis cocher Include Live Activity. Xcode 26 génère automatiquement le squelette d'ActivityConfiguration. Vérifiez que le bundle ID de l'extension est bien parent.bundle.id.LiveActivities : le préfixe doit correspondre à l'app principale, sinon le pairage échoue à l'exécution.

Déclarer ses ActivityAttributes

ActivityAttributes est le contrat de données entre votre application et l'extension. Il doit être Codable, Hashable et idéalement placé dans un framework partagé pour être visible des deux côtés. Voici l'exemple d'une activité de livraison :

import ActivityKit
import Foundation

public struct DeliveryAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        public var status: DeliveryStatus
        public var etaMinutes: Int
        public var driverName: String
        public var lastUpdated: Date

        public init(status: DeliveryStatus, etaMinutes: Int,
                    driverName: String, lastUpdated: Date) {
            self.status = status
            self.etaMinutes = etaMinutes
            self.driverName = driverName
            self.lastUpdated = lastUpdated
        }
    }

    public enum DeliveryStatus: String, Codable, Hashable {
        case preparing, picked, onTheWay, arrived
    }

    public let orderNumber: String
    public let restaurantName: String

    public init(orderNumber: String, restaurantName: String) {
        self.orderNumber = orderNumber
        self.restaurantName = restaurantName
    }
}

Les propriétés en dehors de ContentState (orderNumber, restaurantName) sont immuables pour la durée de vie de l'activité. Toutes les valeurs susceptibles d'évoluer doivent vivre dans ContentState. Sinon, vous serez forcé de terminer puis redémarrer l'activité, ce qui crée un trou visuel sur l'écran verrouillé.

Construire la vue Dynamic Island

Dans le Widget Extension, déclarez une ActivityConfiguration qui décrit les quatre présentations : écran verrouillé, Dynamic Island compacte, Dynamic Island minimale et Dynamic Island étendue. SwiftUI s'occupe du reste, y compris des animations d'inter-état.

import ActivityKit
import SwiftUI
import WidgetKit

struct DeliveryLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            LockScreenView(context: context)
                .activityBackgroundTint(Color.black.opacity(0.8))
                .activitySystemActionForegroundColor(.white)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Label(context.attributes.restaurantName,
                          systemImage: "bag.fill")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("\(context.state.etaMinutes) min")
                        .font(.headline)
                }
                DynamicIslandExpandedRegion(.bottom) {
                    ProgressView(value: progress(for: context.state.status))
                }
            } compactLeading: {
                Image(systemName: "bag.fill")
            } compactTrailing: {
                Text("\(context.state.etaMinutes)m")
            } minimal: {
                Image(systemName: "bag.fill")
            }
            .keylineTint(.orange)
        }
    }

    private func progress(for status: DeliveryAttributes.DeliveryStatus) -> Double {
        switch status {
        case .preparing: return 0.25
        case .picked:    return 0.5
        case .onTheWay:  return 0.75
        case .arrived:   return 1.0
        }
    }
}

Trois règles à retenir : la présentation compacte ne dispose que d'environ 8 caractères côté trailing, la minimale n'accepte qu'une icône, et l'étendue est limitée à 160 points de hauteur. Si vous dépassez, iOS rogne sans avertir et votre UI casse en production. Pour respecter l'esthétique iOS 26, alignez vos couleurs sur les recommandations du design Liquid Glass de SwiftUI.

Démarrer, mettre à jour, terminer

Côté application principale, le cycle de vie d'une activité tient en trois appels. D'abord, vérifiez l'autorisation utilisateur, puis utilisez Activity.request :

import ActivityKit

@MainActor
final class DeliveryActivityManager {
    private var activity: Activity<DeliveryAttributes>?

    func start(orderNumber: String, restaurant: String) async throws {
        guard ActivityAuthorizationInfo().areActivitiesEnabled else {
            throw ActivityError.notAuthorized
        }

        let attributes = DeliveryAttributes(
            orderNumber: orderNumber,
            restaurantName: restaurant
        )
        let initialState = DeliveryAttributes.ContentState(
            status: .preparing,
            etaMinutes: 35,
            driverName: "",
            lastUpdated: .now
        )

        activity = try Activity.request(
            attributes: attributes,
            content: .init(state: initialState,
                           staleDate: .now.addingTimeInterval(60 * 30)),
            pushType: .token
        )
    }

    func update(eta: Int, status: DeliveryAttributes.DeliveryStatus) async {
        guard let activity else { return }
        let newState = DeliveryAttributes.ContentState(
            status: status,
            etaMinutes: eta,
            driverName: "Lucas",
            lastUpdated: .now
        )
        await activity.update(
            ActivityContent(state: newState,
                            staleDate: .now.addingTimeInterval(60 * 15))
        )
    }

    func end() async {
        await activity?.end(dismissalPolicy: .after(.now.addingTimeInterval(60)))
    }

    enum ActivityError: Error { case notAuthorized }
}

Le paramètre pushType: .token demande à iOS d'émettre un token APNs unique pour cette activité, indispensable si vous comptez pousser des mises à jour depuis votre backend. Le staleDate, lui, indique au système quand considérer l'information périmée. Passé ce délai, la Dynamic Island affichera automatiquement un indicateur visuel d'obsolescence.

Mises à jour push via APNs

Pour mettre à jour une Live Activity depuis votre serveur, abonnez-vous au flux de tokens dès le démarrage :

Task {
    for await tokenData in activity.pushTokenUpdates {
        let hexToken = tokenData.map { String(format: "%02x", $0) }.joined()
        await backend.registerLiveActivityToken(hexToken,
                                                activityID: activity.id)
    }
}

Le flux pushTokenUpdates émet à chaque rotation du token (déclenchée par iOS pour des raisons de confidentialité). Ne mettez surtout pas le token en cache, sinon vos pushes finissent silencieusement rejetés par APNs après 24 heures. Honnêtement, c'est le bug le plus pernicieux que j'ai croisé sur ce framework. Côté backend, le payload doit cibler le topic your.bundle.id.push-type.liveactivity avec un en-tête HTTP/2 apns-push-type: liveactivity :

{
  "aps": {
    "timestamp": 1751376000,
    "event": "update",
    "content-state": {
      "status": "onTheWay",
      "etaMinutes": 8,
      "driverName": "Lucas",
      "lastUpdated": "2026-07-01T14:00:00Z"
    },
    "stale-date": 1751376900,
    "alert": {
      "title": "Votre commande arrive",
      "body": "Le chauffeur sera là dans 8 minutes"
    }
  }
}

Quelques limites concrètes : la taille totale du payload est plafonnée à 4 Ko, la fréquence maximale recommandée est d'une mise à jour toutes les 15 secondes, et toute Live Activity inactive depuis 4 heures voit sa priorité chuter. Pour la grille complète des comportements, voir la documentation Apple sur les pushes de Live Activities.

Nouveautés des Live Activities en iOS 26

L'édition iOS 26 introduit trois changements majeurs que tout développeur doit intégrer avant la rentrée 2026 :

1. ActivityFamily.watch

Vos Live Activities s'affichent désormais automatiquement dans la Smart Stack d'Apple Watch sans code supplémentaire, à condition de déclarer supportedFamilies: [.watch, .iPhone] dans la configuration. La présentation watchOS est dérivée de la vue compacte de la Dynamic Island, mais vous pouvez la personnaliser via WidgetFamily.accessoryRectangular.

2. RelevanceScore unifié

Un relevanceScore par ActivityContent détermine l'ordre d'affichage quand l'utilisateur a plusieurs activités simultanées. Le score est un Double entre 0 et 100. Une livraison à 5 minutes (score 80) passera devant un timer de minuterie qui débute (score 20).

3. StaleDate intelligent

Le système n'affiche plus brutalement « obsolète ». À la place, il floute légèrement les valeurs périmées et déclenche un BackgroundTasks.shared.submit pour réveiller silencieusement votre app. Cela élimine 80 % des cas où l'utilisateur fermait la Live Activity par frustration.

Apple a également déprécié l'ancienne signature Activity.update(using:) au profit de Activity.update(_:) avec ActivityContent. Migrez avant iOS 27, l'ancienne API sera retirée. Le changelog complet est consultable dans les notes de version iOS 26.

Comment afficher une Live Activity sur Apple Watch ?

La Smart Stack watchOS 12 (livré avec iOS 26) consomme directement vos Live Activities sans extension watchOS séparée. Le système relaie l'ActivityContent depuis l'iPhone connecté et l'affiche dans une carte rectangulaire de 184×84 points. Trois ajustements sont recommandés :

  • Texte court : ciblez 1 ou 2 mots par ligne. Les libellés Restaurant : ou ETA : n'apportent rien sur si peu de pixels.
  • Couleurs vives : le mode Always-On de la Series 9+ assombrit l'écran de 50 %. Une couleur saturée reste lisible alors qu'un pastel disparaît.
  • Glanceable score : utilisez le relevanceScore pour faire remonter votre activité quand l'événement est imminent.

Sur les modèles antérieurs (Series 7, SE), l'Apple Watch reçoit la Live Activity mais sans la rendre sur le poignet. Votre code reste compatible, simplement la diffusion s'arrête à l'iPhone.

Erreurs fréquentes et debug

Trois symptômes reviennent constamment dans les forums développeurs :

« Activity.request renvoie unsupportedTarget ». Vous avez probablement oublié NSSupportsLiveActivities dans l'Info.plist du target principal (pas celui de l'extension). C'est l'erreur n°1.

« Mes pushes ne déclenchent rien ». Vérifiez l'en-tête apns-push-type: liveactivity et le topic bundle.push-type.liveactivity. Sans cela, APNs renvoie un 200 mais iOS jette silencieusement le payload.

« La Dynamic Island ne s'affiche pas ». Le système masque la Dynamic Island si l'utilisateur a activé le focus Ne pas déranger ou si l'iPhone est sur l'écran d'accueil. Ce n'est pas un bug : utilisez la commande xcrun simctl push ou Console.app filtré sur com.apple.activitykit pour confirmer que l'événement arrive.

Pour intégrer vos Live Activities avec Siri et les Raccourcis (par exemple, démarrer une activité par commande vocale), combinez-les avec le framework App Intents. Un AppIntent qui retourne .result(opensIntent: ...) peut directement lancer Activity.request.

Questions fréquentes

Combien de temps dure une Live Activity au maximum ?

Une Live Activity reste interactive jusqu'à 8 heures, puis bascule en mode « stale » pendant 4 heures supplémentaires avant suppression forcée par iOS. Vous pouvez prolonger l'expérience en démarrant une nouvelle activité avant l'expiration, mais l'utilisateur verra un bref flash visuel.

Faut-il un certificat APNs spécial pour les Live Activities ?

Non, vous utilisez le même certificat ou la même clé Auth Key p8 que pour vos notifications classiques. La distinction se fait au niveau du topic (bundle.id.push-type.liveactivity) et de l'en-tête HTTP/2 apns-push-type: liveactivity, pas du certificat lui-même.

Puis-je tester les Live Activities sans iPhone 14 Pro ?

Oui. L'écran verrouillé fonctionne sur tous les iPhones depuis l'iPhone XS. La Dynamic Island, en revanche, nécessite un iPhone 14 Pro/Plus, 15 (tous modèles) ou plus récent, ou un simulateur Xcode 26 configuré sur ces modèles.

Comment terminer une Live Activity à distance depuis mon backend ?

Envoyez un payload APNs avec "event": "end" et un dismissal-date Unix timestamp. iOS affichera l'activité encore quelques minutes (par défaut 4 heures maximum) puis la retirera automatiquement de l'écran verrouillé.

Les Live Activities consomment-elles beaucoup de batterie ?

Pas si vous respectez les limites : 1 push toutes les 15 secondes maximum, un staleDate raisonnable, et pas d'animations complexes. Les mises à jour push n'ont aucun coût batterie côté client. C'est le rendu à l'écran qui en a un. Désactivez NSSupportsLiveActivitiesFrequentUpdates si vous ne mettez à jour qu'une fois par minute.

Editorial Team
À propos de l'auteur Editorial Team

Our team of expert writers and editors.