← Tous les articles

SwiftData dans iOS 27 : observation et historique

SwiftData est arrivé avec iOS 17 en offrant deux façons de surveiller vos données : @Query à l’intérieur d’une vue SwiftUI, ou une plomberie de notifications manuelle sur ModelContext pour tout le reste. Aucune des deux ne couvrait le cas qui compte le plus pour une application synchronisée, à savoir savoir quand un autre appareil a modifié le store. iOS 27 comble ces deux lacunes en une seule version. ResultsObserver fait du suivi des changements un objet de premier ordre que vous pouvez détenir en dehors d’une vue, et HistoryObserver surveille l’historique persistant de SwiftData et incrémente un compteur observable lorsque de nouvelles transactions arrivent, de sorte que votre code de synchronisation peut récupérer uniquement les derniers changements. iOS 27 ajoute l’observation comme une primitive, et non comme un effet de bord de SwiftUI.1

Le cadrage rejoint celui du reste de cette série : SwiftData était bon marché au démarrage et coûteux à coordonner. Surveiller un fetch en dehors de la hiérarchie de vues revenait à reconstruire à la main ce que fait @Query. Maintenir un store synchronisé avec un serveur externe, ou réagir aux écritures provenant d’une extension d’application, signifiait parcourir vous-même l’historique persistant et réconcilier les transactions. iOS 27 confie à chacune de ces tâches un type nommé qui se conforme à Observable, de sorte que la même machinerie de mise à jour SwiftUI qui pilote déjà vos vues pilote également votre couche de synchronisation.

En bref / Points clés

  • ResultsObserver observe et suit les changements apportés à une collection de modèles persistants dans un model context, fournissant des mises à jour en temps réel lorsque les données sous-jacentes changent. Il est Observable, de sorte qu’une vue SwiftUI se met à jour automatiquement, et il fonctionne en dehors d’une vue là où @Query ne peut pas atteindre.2
  • HistoryObserver observe l’historique persistant de SwiftData et expose une seule propriété observable, eventCounter, qui s’incrémente lorsque de nouvelles transactions arrivent. Vous filtrez par type de modèle et par auteur de transaction, puis vous appelez ModelContext.fetchHistory pour lire les changements. C’est la réponse structurée pour la synchronisation avec un serveur externe ou la réaction aux écritures d’une extension d’application.3
  • @Attribute(.codable) utilise la représentation codable de la propriété pour la stocker, ce qui vous donne une façon déclarative de persister un type valeur Codable sans ValueTransformer.4
  • Les trois sont livrés sur iOS, iPadOS, macOS, Mac Catalyst, tvOS, visionOS et watchOS dans la bêta 27.0.234

Surveiller un fetch en dehors de la vue : ResultsObserver

@Query est excellent et contraint. Il vit dans une vue SwiftUI, il se réexécute lorsque son prédicat change, et il remet un tableau à la vue. La contrainte tient à l’emplacement. Un view model, un coordinateur de synchronisation, une tâche d’export ou un réconciliateur d’arrière-plan possède des données qui changent et n’a aucun @Query sur lequel s’appuyer. Avant iOS 27, ces appelants s’abonnaient aux notifications de sauvegarde de ModelContext et re-fetchaient à la main, ce qui constitue la reconstruction manuelle de ce que @Query fait déjà en interne.

ResultsObserver est la réponse nommée d’iOS 27. La déclaration énonce ce qu’il est :2

final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable

La classe surveille automatiquement les changements apportés aux modèles qui correspondent à des critères de fetch spécifiés et maintient une collection de résultats récupérés, ce qui en fait l’outil pour garder n’importe quel consommateur synchronisé avec les données persistantes, et pas seulement une vue.2 Vous la configurez de deux façons : avec un FetchDescriptor complet, ou avec des prédicats de filtre et des descripteurs de tri individuels.2 Les deux chemins SectionName comptent pour les listes groupées ; lorsque vous n’avez pas besoin de sectionnement, vous passez Never comme paramètre de type SectionName.2

Le gain par rapport à l’ancienne approche manuelle est la conformité Observable. ResultsObserver est Observable, ce qui permet aux vues SwiftUI de se mettre à jour automatiquement lorsque les résultats changent, de la même manière que les objets modèles @Observable pilotent l’invalidation de vue (abordé dans les rouages internes de @Observable).2 Un coordinateur de synchronisation qui détient un ResultsObserver reçoit des notifications de changement sans écrire une seule ligne de NotificationCenter, et toute vue qui lit les résultats de l’observer est rendue à nouveau gratuitement.

Lors de la session 274, Apple présente ResultsObserver pour le cas que @Query ne peut pas servir : récupérer et observer le store depuis n’importe où dans votre application via Swift Observation, y compris un state object ou un jeu qui ne touche jamais à SwiftUI.5

Watch on Apple Developer ↗
ResultsObserver apporte l’observation de style requête au code situé en dehors des vues SwiftUI.

import SwiftData
import Observation

@Observable
final class ShoppingListModel {
    let observer: ResultsObserver<ShoppingItem, Never>

    init(context: ModelContext) {
        let descriptor = FetchDescriptor<ShoppingItem>(
            sortBy: [SortDescriptor(\.sortOrder)]
        )
        // No sectioning, so SectionName is Never.
        observer = ResultsObserver(context: context, fetchDescriptor: descriptor)
    }
}

La référence publiée par Apple confirme les déclarations de classe et la surface de configuration (un FetchDescriptor ou des prédicats de filtre et des descripteurs de tri, Never pour le nom de section lorsque vous ne sectionnez pas) mais élude les signatures exactes des initialiseurs au moment de la rédaction ; considérez donc les formes d’appel de ces exemples comme illustratives et confirmez les libellés de paramètres par rapport au SDK.

Le glissement mental, c’est que le fetch devient un objet que vous possédez et que vous faites circuler, plutôt qu’un property wrapper piégé dans le body d’une vue. Un @Query répond à la question « que montre cette vue ? » Un ResultsObserver répond à la question « quel est l’état actuel de ce fetch, où que je le détienne ? » La seconde question est celle que pose réellement un appelant qui n’est pas une vue.

Réagir aux changements de l’historique : HistoryObserver

La lacune que @Query et ResultsObserver laissent tous deux ouverte, c’est le changement qui prend naissance en dehors de votre fetch in-process. Chaque fois que votre store est sauvegardé, SwiftData enregistre une transaction d’historique décrivant ce qui a changé, d’où provient le changement, et un jeton qui l’identifie. Maintenir un store synchronisé avec un serveur externe, ou réagir aux écritures provenant d’une extension d’application, signifiait parcourir vous-même cet historique persistant. iOS 27 confie cette tâche à un observer nommé.3

HistoryObserver est la réponse structurée :3

final class HistoryObserver

L’observer surveille l’historique persistant de SwiftData et permet à votre code de réagir lorsque de nouvelles transactions sont ajoutées.3 Si vous n’avez besoin que de certains types de changements, vous filtrez par type de modèle et par auteur de transaction, de sorte que vous réagissez aux écritures qui vous importent plutôt qu’à chaque mutation du store.3

Toute la surface tient en une seule propriété observable, eventCounter. Lorsque de nouvelles transactions arrivent dans l’historique persistant, le compteur s’incrémente ; vous l’observez, et à chaque incrément vous appelez l’API ModelContext.fetchHistory pour ne lire que les derniers changements.3 Les jetons d’historique vivent sur les transactions elles-mêmes, de sorte que fetchHistory ne retourne que ce qui est nouveau au lieu de re-parcourir le store. Cela garde un gestionnaire de synchronisation rapide à mesure que l’historique grandit.

import SwiftData
import Observation

// Illustrative call shapes; confirm parameter labels against the SDK.
let historyObserver = HistoryObserver(container: modelContainer, authors: "App")

// Observe eventCounter; on each increment, fetch and process the new history.
let token = withContinuousObservation(of: historyObserver.eventCounter) {
    Task { await processChanges() }
}

Le filtrage par auteur de transaction est le détail qui rend correcte la synchronisation serveur. Dans l’exemple d’Apple, l’observer passe "App" comme auteur afin de ne réagir qu’aux changements effectués par l’application, et de ne pas rejouer vers le serveur les changements qui en provenaient.3 À chaque incrément, votre étape processChanges appelle ModelContext.fetchHistory pour lire les nouvelles transactions et les téléverser. Les schémas multiprocessus et de synchronisation externe que les applications de cette série résolvaient avec une plomberie d’historique faite à la main (voir la discipline de schéma SwiftData) disposent désormais d’une couture du framework sur laquelle s’accrocher.

Des lectures légères qui s’associent à l’historique

Une fois que HistoryObserver réveille votre gestionnaire de synchronisation, la question suivante est de savoir combien de travail effectuer. Un gestionnaire naïf re-fetche les modèles affectés à chaque incrément, ce qui hydrate des objets modèles complets dont vous n’avez peut-être pas besoin. SwiftData vous offre deux lectures qui répondent à des questions plus étroites sans rien matérialiser, et toutes deux s’associent naturellement à l’observation de l’historique.

ModelContext.fetchCount(_:) retourne le nombre de modèles qui correspondent à un fetch descriptor sous la forme d’un simple Int, sans charger les objets correspondants :6

func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel

ModelContext.fetchIdentifiers(_:) retourne les correspondances sous la forme d’un [PersistentIdentifier], là encore sans charger les modèles qui se trouvent derrière. Une surcharge batchSize diffuse ces identifiants par lots pour les grands ensembles de résultats :6

func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel

Le SwiftData Group Lab a suggéré un schéma qui relie celles-ci directement à l’observation de l’historique. Lorsqu’un changement d’historique arrive, récupérez les identifiants affectés et comparez-les à ce que la vue affiche réellement avant de décider s’il faut recharger, afin d’éviter d’hydrater des objets dont vous n’avez pas besoin.6 Un changement sur une ligne que l’utilisateur ne regarde pas ne déplace aucun pixel, et une comparaison d’identifiants vous l’indique pour le coût d’une recherche par clé plutôt que d’un fetch complet. fetchCount répond à la question encore moins coûteuse de savoir si quelque chose a correspondu du tout, ce qui suffit pour décider si un badge ou un état vide doit basculer.

Persister proprement un type valeur : @Attribute(.codable)

Le troisième ajout est petit et pratique. SwiftData stocke nativement les types primitifs de Swift et les relations @Model, mais une propriété dont le type est une valeur Codable personnalisée (une structure contenant quelques champs, une énumération avec des valeurs associées) nécessitait un ValueTransformer et l’attribut .transformable(by:), soit du cérémonial Core Data qui ressurgit à travers la surface des macros.

iOS 27 ajoute l’option de stockage codable :4

static var codable: Schema.Attribute.Option { get }

L’option utilise la représentation codable de la propriété pour la stocker, de sorte qu’un type valeur Codable persiste à travers sa propre conformité Encodable/Decodable sans aucun transformer à enregistrer.4 Vous l’appliquez de la même manière que n’importe quelle autre option d’attribut :

import SwiftData

struct Coordinate: Codable {
    var latitude: Double
    var longitude: Double
}

@Model
final class Place {
    var name: String

    // Persisted via Coordinate's own Codable conformance.
    @Attribute(.codable) var location: Coordinate
}

La règle pratique est de se tourner vers .codable lorsqu’une propriété est une valeur Codable autonome qui ne mérite pas sa propre table @Model. Une paire de coordonnées, une petite structure de paramètres, une énumération avec une charge utile : ce sont des données, pas des entités, et .codable les stocke en ligne à travers la représentation qu’elles définissent déjà au lieu de forcer un transformer ou une relation artificielle.

Apple est explicite sur les compromis. Le contenu d’un attribut codable est opaque pour SwiftData, de sorte que vous ne pouvez pas l’utiliser dans des prédicats pour filtrer les résultats ni dans des descripteurs de tri, et un changement dans la forme du type codable (ajout ou suppression de propriétés) ne déclenchera pas de migration ; son implémentation Codable doit donc rester compatible vers l’avant et vers l’arrière.5 Apple présente .codable comme une trappe de secours pour les types que vous ne possédez pas ; pour les types que vous définissez, les modéliser en tant que modèles SwiftData ou en tant que types valeur pris en charge conserve le tri, le filtrage et l’indexation sur la table.5

Quand se tourner vers chacun

Les trois ajouts répondent à trois questions différentes, et la question vous dit lequel utiliser.

  • Tournez-vous vers ResultsObserver lorsqu’un appelant qui n’est pas une vue a besoin d’un fetch en direct. Un view model, un coordinateur, une tâche d’export, tout ce qui possède des données qui changent et qui n’est pas un body SwiftUI. À l’intérieur d’une vue, @Query reste l’outil le plus léger ; l’observer gagne sa place dès l’instant où le consommateur n’est pas une vue.2
  • Tournez-vous vers HistoryObserver lorsque vous synchronisez le store avec quelque chose en dehors de votre application. Un serveur externe, ou une extension d’application qui écrit dans le même store. Observez eventCounter, filtrez par type de modèle et par auteur de transaction, et appelez ModelContext.fetchHistory à chaque incrément pour ne lire que les nouvelles transactions.3
  • Tournez-vous vers @Attribute(.codable) lorsqu’une propriété est une valeur Codable, et non une entité. De petites structures et énumérations qui voyagent avec leur propriétaire. Si le type a besoin de sa propre identité, de relations ou de requêtes, c’est @Model qu’il lui faut ; si ce ne sont que des données en ligne, .codable fait l’économie du transformer.4

Les deux observers se composent. Un ResultsObserver garde votre fetch in-process en direct ; un HistoryObserver vous indique quand un push distant justifie d’agir sur ce qui a changé. Une application qui réalise une véritable synchronisation multi-appareils utilise les deux, et utilise .codable pour garder honnêtes ses colonnes de type valeur au passage.

FAQ

En quoi ResultsObserver diffère-t-il de @Query ?

@Query est un property wrapper SwiftUI qui vit à l’intérieur d’une vue et alimente cette vue avec un tableau. ResultsObserver est une classe autonome que vous créez et détenez n’importe où, y compris en dehors de la hiérarchie de vues, qui observe et suit les changements apportés à une collection de modèles persistants dans un model context.2 Parce que l’observer est Observable, une vue SwiftUI qui le lit se met toujours à jour automatiquement ; il couvre donc à la fois le cas in-view et le cas view model ou coordinateur que @Query ne peut pas atteindre.2

Qu’observe réellement HistoryObserver ?

Il observe l’historique persistant de SwiftData, l’enregistrement des transactions que SwiftData écrit chaque fois que le store est sauvegardé.3 Il expose une seule propriété observable, eventCounter, qui s’incrémente lorsque de nouvelles transactions sont disponibles ; vous pouvez filtrer par type de modèle et par auteur de transaction afin que seuls les changements qui vous importent fassent bouger le compteur.3 À chaque incrément, votre code appelle l’API ModelContext.fetchHistory pour lire les nouvelles transactions, ce qui en fait le gestionnaire structuré pour la synchronisation avec un serveur externe ou la réaction aux écritures d’une extension d’application.3

Puis-je utiliser ResultsObserver et HistoryObserver ensemble ?

Oui, et une application synchronisée le devrait généralement. ResultsObserver garde un fetch in-process à jour à mesure que le contexte local change ; HistoryObserver fait remonter les changements enregistrés dans l’historique persistant, filtrés par type de modèle et par auteur de transaction.23 Tous deux sont des objets observables auxquels vous pouvez réagir depuis une vue SwiftUI ou un autre observer, de sorte qu’ils s’insèrent dans le même flux réactif sans gestion de notifications distincte.23

Quand dois-je utiliser @Attribute(.codable) plutôt qu’une relation ?

Utilisez .codable lorsque la propriété est un type valeur Codable autonome qui n’a pas d’identité indépendante, puisque l’option stocke la propriété à travers sa propre représentation codable.4 Utilisez une relation @Model lorsque la valeur est une véritable entité dotée de son propre cycle de vie, de sa propre identité ou de ses propres requêtes. La ligne de partage tient à ce que la chose soit une donnée qui appartient à son propriétaire, ou une entité que d’autres lignes référencent.

La série complète Apple Ecosystem : la discipline de schéma SwiftData pour le coût de migration sur lequel l’observation se pose ; le guide des migrations SwiftData pour la machinerie VersionedSchema et MigrationPlan ; les rouages internes de @Observable pour le modèle d’observation auquel ces classes se branchent ; les rouages internes de SwiftUI pour le substrat de framework qui se trouve en dessous. Le hub se trouve à la série Apple Ecosystem. Pour un contexte plus large autour d’iOS avec des agents IA, consultez le guide de développement d’agents iOS.

Références


  1. Documentation pour développeurs Apple : SwiftData. La référence du framework couvrant @Model, ModelContext, ModelContainer, les requêtes et les ajouts d’observation d’iOS 27. 

  2. Documentation pour développeurs Apple : ResultsObserver (bêta iOS 27.0). « Observes and tracks changes to a collection of persistent models in a model context. » Déclaré comme final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable ; configurable avec un FetchDescriptor ou avec des prédicats de filtre et des descripteurs de tri ; Observable, de sorte que les vues SwiftUI se mettent à jour automatiquement ; passez Never comme SectionName lorsqu’aucun sectionnement n’est nécessaire. 

  3. Apple, session WWDC26 274, What’s new in SwiftData, et Documentation pour développeurs Apple : HistoryObserver (bêta iOS 27.0). Déclaré comme final class HistoryObserver. D’après la session 274, il observe l’historique persistant de SwiftData et « has a single observable property » (eventCounter) ; « when new transactions are available in the persistent history, the eventCounter increments », et « your code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes ». Il « lets you filter by model type and transaction author » ; l’exemple de la session passe "App" comme auteur afin que les changements provenant de l’application ne soient pas rejoués vers un serveur externe. 

  4. Documentation pour développeurs Apple : codable (bêta iOS 27.0). « Uses the property’s codable representation to store the property. » Déclaré comme static var codable: Schema.Attribute.Option { get }

  5. Apple, session WWDC26 274, What’s new in SwiftData. Apple présente ResultsObserver, qui « fetches data from your SwiftData store and then observes your store for changes » mais « works anywhere in your app (independent of SwiftUI views) using Swift Observation », et cite un state object ou un jeu écrit en SceneKit comme des cas que @Query ne peut pas atteindre. 

  6. Documentation pour développeurs Apple : fetchCount(_:) et fetchIdentifiers(_:) sur ModelContext. fetchCount est déclaré func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel et « returns the number of models that match the criteria of the specified fetch descriptor ». fetchIdentifiers est déclaré func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel et retourne « an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria » ; une surcharge fetchIdentifiers(_:batchSize:) retourne les identifiants par lots sous la forme d’un FetchResultsCollection<PersistentIdentifier>. Source du schéma récupérer-les-identifiants-affectés-puis-comparer-à-la-vue : paraphrasé d’un enregistrement transcrit localement du SwiftData Group Lab de la WWDC 2026 ; Apple ne publie aucun sous-titre officiel pour les labs. 

Articles connexes

Migrations SwiftData : légères vs personnalisées, et quand vous n'avez pas besoin d'un V2

Le modèle de migration de SwiftData repose sur VersionedSchema, MigrationStage et SchemaMigrationPlan. La plupart des ch…

15 min de lecture

Le vrai coût de SwiftData, c'est la discipline du schéma

L'API de SwiftData se résume à deux macros. Le coût, c'est ce qui se passe après la mise en production. Les champs optio…

18 min de lecture

The Robots Are Taking Exams in My Search Console

First-party GSC data: 91% of 3.8M impressions fail a human-query filter. Exam questions, pasted errors, and agent sweeps…

10 min de lecture