SwiftData in iOS 27: Observation und History
SwiftData erschien mit iOS 17 und bot zwei Möglichkeiten, Ihre Daten zu beobachten: @Query innerhalb einer SwiftUI-View oder manuelle Notification-Verkabelung am ModelContext für alles andere. Keine davon deckte den Fall ab, der für eine synchronisierte App am wichtigsten ist, nämlich zu wissen, wann ein anderes Gerät den Store verändert hat. iOS 27 schließt beide Lücken in einer einzigen Version. ResultsObserver macht Change Tracking zu einem erstklassigen Objekt, das Sie außerhalb einer View halten können, und HistoryObserver beobachtet SwiftDatas Persistent History und inkrementiert einen beobachtbaren Zähler, sobald neue Transaktionen eintreffen, sodass Ihr Sync-Code nur die jeweils neuesten Änderungen abrufen kann. iOS 27 fügt Observation als Primitiv hinzu, nicht als SwiftUI-Nebeneffekt.1
Die Einordnung passt zum Rest dieses Clusters: SwiftData war günstig im Einstieg und teuer in der Koordination. Einen Fetch außerhalb der View-Hierarchie zu beobachten bedeutete, das, was @Query tut, von Hand nachzubauen. Einen Store mit einem externen Server synchron zu halten oder auf Schreibvorgänge aus einer App-Extension zu reagieren, bedeutete, die Persistent History selbst zu durchlaufen und Transaktionen abzugleichen. iOS 27 gibt beiden Aufgaben einen benannten Typ, der Observable entspricht, sodass dieselbe SwiftUI-Update-Maschinerie, die bereits Ihre Views antreibt, auch Ihre Sync-Schicht antreibt.
TL;DR / Kernpunkte
ResultsObserverbeobachtet und verfolgt Änderungen an einer Sammlung von Persistent Models in einem Model Context und liefert Echtzeit-Updates, wenn sich die zugrunde liegenden Daten ändern. Er istObservable, sodass sich eine SwiftUI-View automatisch aktualisiert, und er funktioniert außerhalb einer View, wo@Querynicht hinreicht.2HistoryObserverbeobachtet SwiftDatas Persistent History und stellt eine einzige beobachtbare Eigenschaft bereit,eventCounter, die inkrementiert, sobald neue Transaktionen eintreffen. Sie filtern nach Model-Typ und Transaktions-Author und rufen dannModelContext.fetchHistoryauf, um die Änderungen zu lesen. Das ist die strukturierte Antwort für die Synchronisierung mit einem externen Server oder für die Reaktion auf Schreibvorgänge aus App-Extensions.3@Attribute(.codable)nutzt die Codable-Repräsentation der Eigenschaft, um diese zu speichern, und gibt Ihnen damit eine deklarative Möglichkeit, einenCodable-Werttyp ohneValueTransformerzu persistieren.4- Alle drei erscheinen plattformübergreifend für iOS, iPadOS, macOS, Mac Catalyst, tvOS, visionOS und watchOS in der 27.0-Beta.234
Einen Fetch außerhalb der View beobachten: ResultsObserver
@Query ist hervorragend und eingeschränkt zugleich. Es lebt in einer SwiftUI-View, läuft erneut, wenn sich sein Prädikat ändert, und übergibt der View ein Array. Die Einschränkung ist der Ort. Ein View-Model, ein Sync-Koordinator, ein Export-Job oder ein Hintergrund-Reconciler hat Daten, die sich ändern, und kein @Query, auf das er sich stützen kann. Vor iOS 27 abonnierten solche Aufrufer die Save-Notifications des ModelContext und holten die Daten von Hand erneut ab, was genau die manuelle Rekonstruktion dessen ist, was @Query intern bereits leistet.
ResultsObserver ist die benannte Antwort von iOS 27. Die Deklaration besagt, worum es sich handelt:2
final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable
Die Klasse überwacht automatisch Änderungen an Models, die festgelegten Fetch-Kriterien entsprechen, und pflegt eine Sammlung abgerufener Ergebnisse, was sie zum Werkzeug macht, um jeden Konsumenten mit persistenten Daten synchron zu halten, nicht nur eine View.2 Sie konfigurieren ihn auf zwei Arten: mit einem vollständigen FetchDescriptor oder mit einzelnen Filterprädikaten und Sort Descriptors.2 Die beiden SectionName-Pfade sind für gruppierte Listen von Bedeutung; wenn Sie keine Sektionierung benötigen, übergeben Sie Never als den SectionName-Typparameter.2
Der Gewinn gegenüber dem alten manuellen Ansatz ist die Observable-Konformität. ResultsObserver ist Observable, was SwiftUI-Views erlaubt, sich automatisch zu aktualisieren, wenn sich die Ergebnisse ändern, genauso wie @Observable-Model-Objekte die View-Invalidierung antreiben (behandelt in @Observable Internals).2 Ein Sync-Koordinator, der einen ResultsObserver hält, erhält Änderungsbenachrichtigungen, ohne eine einzige NotificationCenter-Zeile zu schreiben, und jede View, die die Ergebnisse des Observers liest, rendert kostenlos neu.
In Session 274 stellt Apple ResultsObserver für den Fall vor, den @Query nicht bedienen kann: den Store von überall in Ihrer App über Swift Observation abzurufen und zu beobachten, einschließlich eines State-Objekts oder eines Spiels, das SwiftUI nie berührt.5
ResultsObserver bringt Query-artige Observation in Code außerhalb von SwiftUI-Views.
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)
}
}
Apples veröffentlichte Referenz bestätigt die Klassendeklarationen und die Konfigurationsfläche (ein FetchDescriptor oder Filterprädikate und Sort Descriptors, Never für den Section-Namen, wenn Sie nicht sektionieren), lässt aber die exakten Initializer-Signaturen zum Zeitpunkt der Erstellung aus, behandeln Sie die Aufrufformen in diesen Beispielen daher als illustrativ und bestätigen Sie die Parameterbezeichnungen anhand des SDK.
Die mentale Verschiebung besteht darin, dass der Fetch zu einem Objekt wird, das Sie besitzen und herumreichen, anstatt zu einem Property Wrapper, der im body einer View gefangen ist. Ein @Query beantwortet die Frage „Was zeigt diese View an?” Ein ResultsObserver beantwortet die Frage „Was ist der aktuelle Zustand dieses Fetch, wo auch immer ich ihn halte?” Die zweite Frage ist diejenige, die ein Nicht-View-Aufrufer tatsächlich stellt.
Auf History-Änderungen reagieren: HistoryObserver
Die Lücke, die @Query und ResultsObserver beide offen lassen, ist die Änderung, die außerhalb Ihres In-Process-Fetch entsteht. Jedes Mal, wenn Ihr Store gespeichert wird, zeichnet SwiftData eine History-Transaktion auf, die beschreibt, was sich geändert hat, woher die Änderung kam, und ein Token, das sie identifiziert. Einen Store mit einem externen Server synchron zu halten oder auf Schreibvorgänge aus einer App-Extension zu reagieren, bedeutete, diese Persistent History selbst zu durchlaufen. iOS 27 gibt der Aufgabe einen benannten Observer.3
HistoryObserver ist die strukturierte Antwort:3
final class HistoryObserver
Der Observer beobachtet SwiftDatas Persistent History und lässt Ihren Code reagieren, wenn neue Transaktionen hinzugefügt werden.3 Wenn Sie nur bestimmte Arten von Änderungen benötigen, filtern Sie nach Model-Typ und Transaktions-Author, sodass Sie auf die Schreibvorgänge reagieren, die Ihnen wichtig sind, statt auf jede Store-Mutation.3
Die gesamte Fläche ist eine beobachtbare Eigenschaft, eventCounter. Wenn neue Transaktionen in der Persistent History eintreffen, inkrementiert der Zähler; Sie beobachten ihn und rufen bei jedem Inkrement die ModelContext.fetchHistory-API auf, um nur die neuesten Änderungen zu lesen.3 Die History-Tokens liegen auf den Transaktionen selbst, sodass fetchHistory nur das zurückgibt, was neu ist, statt den Store erneut zu durchsuchen. Das hält einen Sync-Handler schnell, während die History wächst.
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() }
}
Das Filtern nach Transaktions-Author ist das Detail, das die Server-Synchronisierung korrekt macht. In Apples Beispiel übergibt der Observer "App" als Author, sodass er nur auf Änderungen reagiert, die die App vorgenommen hat, und keine Änderungen, die vom Server kamen, an den Server zurückspielt.3 Bei jedem Inkrement ruft Ihr processChanges-Schritt ModelContext.fetchHistory auf, um die neuen Transaktionen zu lesen und hochzuladen. Die Muster für Multi-Process- und externe Synchronisierung, die die Apps in diesem Cluster mit selbstgebauter History-Verkabelung lösten (siehe SwiftData-Schema-Disziplin), erhalten eine Framework-Naht, an der sie sich aufhängen können.
Leichtgewichtige Reads, die mit der History zusammenspielen
Sobald HistoryObserver Ihren Sync-Handler weckt, ist die nächste Frage, wie viel Arbeit zu tun ist. Ein naiver Handler holt die betroffenen Models bei jedem Inkrement erneut ab, was vollständige Model-Objekte hydratisiert, die Sie möglicherweise nicht benötigen. SwiftData gibt Ihnen zwei Reads, die engere Fragen beantworten, ohne irgendetwas zu materialisieren, und beide spielen natürlich mit der History-Beobachtung zusammen.
ModelContext.fetchCount(_:) gibt die Anzahl der Models zurück, die einem Fetch Descriptor entsprechen, als einfaches Int, ohne die passenden Objekte zu laden:6
func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel
ModelContext.fetchIdentifiers(_:) gibt die Treffer als [PersistentIdentifier] zurück, wiederum ohne die dahinterstehenden Models zu laden. Eine batchSize-Überladung streamt diese Identifier in Chunks für große Ergebnismengen:6
func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel
Das SwiftData Group Lab schlug ein Muster vor, das diese direkt mit der History-Beobachtung verbindet. Wenn eine History-Änderung eintrifft, holen Sie die betroffenen Identifier und vergleichen sie mit dem, was die View tatsächlich anzeigt, bevor Sie entscheiden, ob neu geladen werden soll, sodass Sie das Hydratisieren von Objekten vermeiden, die Sie nicht benötigen.6 Eine Änderung an einer Zeile, die der Benutzer nicht ansieht, bewegt keine Pixel, und ein Identifier-Vergleich teilt Ihnen das zum Preis eines Key-Lookups statt eines vollständigen Fetch mit. fetchCount beantwortet die noch günstigere Frage, ob überhaupt etwas zugetroffen hat, was ausreicht, um zu entscheiden, ob ein Badge oder ein Leerzustand umschlagen muss.
Einen Werttyp sauber persistieren: @Attribute(.codable)
Die dritte Ergänzung ist klein und praktisch. SwiftData speichert Swifts primitive Typen und @Model-Beziehungen nativ, aber eine Eigenschaft, deren Typ ein benutzerdefinierter Codable-Wert ist (ein Struct mit einigen Feldern, ein Enum mit zugeordneten Werten), benötigte einen ValueTransformer und das .transformable(by:)-Attribut, was Core-Data-Zeremoniell ist, das durch die Macro-Fläche zurücksickert.
iOS 27 fügt die codable-Speicheroption hinzu:4
static var codable: Schema.Attribute.Option { get }
Die Option nutzt die Codable-Repräsentation der Eigenschaft, um diese zu speichern, sodass ein Codable-Werttyp über seine eigene Encodable/Decodable-Konformität persistiert wird, ohne dass ein Transformer zu registrieren ist.4 Sie wenden sie auf dieselbe Weise an wie jede andere Attributoption:
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
}
Die praktische Regel lautet, zu .codable zu greifen, wenn eine Eigenschaft ein in sich abgeschlossener Codable-Wert ist, der keine eigene @Model-Tabelle verdient. Ein Koordinatenpaar, ein kleines Settings-Struct, ein Enum mit Payload: Das sind Daten, keine Entitäten, und .codable speichert sie inline über die Repräsentation, die sie ohnehin bereits definieren, anstatt einen Transformer oder eine künstliche Beziehung zu erzwingen.
Apple ist bezüglich der Kompromisse deutlich. Der Inhalt eines Codable-Attributs ist für SwiftData undurchsichtig, sodass Sie ihn weder in Prädikaten zum Filtern von Ergebnissen noch in Sort Descriptors verwenden können, und eine Änderung der Form des Codable-Typs (Hinzufügen oder Entfernen von Eigenschaften) löst keine Migration aus, weshalb seine Codable-Implementierung vorwärts- und rückwärtskompatibel bleiben muss.5 Apple stellt .codable als Notausstieg für Typen dar, die Sie nicht besitzen; bei Typen, die Sie selbst definieren, hält das Modellieren als SwiftData-Models oder unterstützte Werttypen Sortierung, Filterung und Indizierung auf der Tabelle.5
Wann Sie zu welchem greifen sollten
Die drei Ergänzungen beantworten drei verschiedene Fragen, und die Frage sagt Ihnen, welche Sie verwenden sollten.
- Greifen Sie zu
ResultsObserver, wenn ein Nicht-View-Aufrufer einen Live-Fetch benötigt. Ein View-Model, ein Koordinator, eine Export-Task, alles, was Daten hat, die sich ändern, und kein SwiftUI-bodyist. Innerhalb einer View ist@Queryweiterhin das leichtere Werkzeug; der Observer verdient seinen Platz in dem Moment, in dem der Konsument keine View ist.2 - Greifen Sie zu
HistoryObserver, wenn Sie den Store mit etwas außerhalb Ihrer App synchronisieren. Einem externen Server oder einer App-Extension, die in denselben Store schreibt. Beobachten SieeventCounter, filtern Sie nach Model-Typ und Transaktions-Author und rufen Sie bei jedem InkrementModelContext.fetchHistoryauf, um nur die neuen Transaktionen zu lesen.3 - Greifen Sie zu
@Attribute(.codable), wenn eine Eigenschaft einCodable-Wert ist, keine Entität. Kleine Structs und Enums, die mit ihrem Besitzer reisen. Wenn der Typ seine eigene Identität, Beziehungen oder Queries benötigt, will er stattdessen@Model; wenn er nur Inline-Daten ist, überspringt.codableden Transformer.4
Die beiden Observer lassen sich kombinieren. Ein ResultsObserver hält Ihren In-Process-Fetch live; ein HistoryObserver teilt Ihnen mit, wann ein Remote-Push es rechtfertigt, auf das Geänderte zu reagieren. Eine App, die echte Multi-Device-Synchronisierung betreibt, nutzt beide und verwendet .codable, um ihre Werttyp-Spalten dabei ehrlich zu halten.
FAQ
Wie unterscheidet sich ResultsObserver von @Query?
@Query ist ein SwiftUI Property Wrapper, der innerhalb einer View lebt und dieser View ein Array zuführt. ResultsObserver ist eine eigenständige Klasse, die Sie überall erstellen und halten können, auch außerhalb der View-Hierarchie, und die Änderungen an einer Sammlung von Persistent Models in einem Model Context beobachtet und verfolgt.2 Da der Observer Observable ist, aktualisiert sich eine SwiftUI-View, die ihn liest, weiterhin automatisch, sodass er sowohl den In-View-Fall als auch den View-Model- oder Koordinator-Fall abdeckt, den @Query nicht erreichen kann.2
Was beobachtet HistoryObserver eigentlich?
Er beobachtet SwiftDatas Persistent History, die Aufzeichnung der Transaktionen, die SwiftData jedes Mal schreibt, wenn der Store gespeichert wird.3 Er stellt eine einzige beobachtbare Eigenschaft bereit, eventCounter, die inkrementiert, wenn neue Transaktionen verfügbar sind; Sie können nach Model-Typ und Transaktions-Author filtern, sodass nur die Änderungen, die Ihnen wichtig sind, den Zähler bewegen.3 Bei jedem Inkrement ruft Ihr Code die ModelContext.fetchHistory-API auf, um die neuen Transaktionen zu lesen, was ihn zum strukturierten Handler für die Synchronisierung mit einem externen Server oder für die Reaktion auf Schreibvorgänge aus App-Extensions macht.3
Kann ich ResultsObserver und HistoryObserver zusammen verwenden?
Ja, und eine synchronisierte App sollte das in der Regel tun. ResultsObserver hält einen In-Process-Fetch aktuell, während sich der lokale Context ändert; HistoryObserver bringt Änderungen ans Licht, die in der Persistent History aufgezeichnet wurden, gefiltert nach Model-Typ und Transaktions-Author.23 Beide sind beobachtbare Objekte, auf die Sie aus einer SwiftUI-View oder einem anderen Observer reagieren können, sodass sie sich in denselben reaktiven Ablauf einfügen, ohne separate Notification-Behandlung.23
Wann sollte ich @Attribute(.codable) statt einer Beziehung verwenden?
Verwenden Sie .codable, wenn die Eigenschaft ein in sich abgeschlossener Codable-Werttyp ist, der keine eigenständige Identität hat, da die Option die Eigenschaft über ihre eigene Codable-Repräsentation speichert.4 Verwenden Sie eine @Model-Beziehung, wenn der Wert eine echte Entität mit eigenem Lebenszyklus, eigener Identität oder eigenen Queries ist. Die Trennlinie verläuft danach, ob die Sache Daten sind, die ihrem Besitzer gehören, oder eine Entität, auf die andere Zeilen verweisen.
Das vollständige Apple-Ecosystem-Cluster: SwiftData-Schema-Disziplin für die Migrationskosten, auf denen Observation aufsetzt; der SwiftData-Migrations-Leitfaden für die VersionedSchema- und MigrationPlan-Maschinerie; @Observable Internals für das Observation-Modell, in das sich diese Klassen einklinken; SwiftUI Internals für das darunterliegende Framework-Substrat. Der Hub befindet sich bei der Apple Ecosystem Series. Für einen breiteren Kontext zu iOS mit KI-Agenten siehe den iOS Agent Development Guide.
Referenzen
-
Apple Developer Documentation: SwiftData. Die Framework-Referenz, die
@Model,ModelContext,ModelContainer, Queries und die Observation-Ergänzungen von iOS 27 abdeckt. ↩ -
Apple Developer Documentation:
ResultsObserver(iOS 27.0 beta). „Observes and tracks changes to a collection of persistent models in a model context.” Deklariert alsfinal class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable; konfigurierbar mit einemFetchDescriptoroder mit Filterprädikaten und Sort Descriptors;Observable, sodass sich SwiftUI-Views automatisch aktualisieren; übergeben SieNeveralsSectionName, wenn keine Sektionierung benötigt wird. ↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, WWDC26 Session 274, What’s new in SwiftData, und Apple Developer Documentation:
HistoryObserver(iOS 27.0 beta). Deklariert alsfinal class HistoryObserver. Laut Session 274 beobachtet er SwiftDatas Persistent History und „has a single observable property” (eventCounter); „when new transactions are available in the persistent history, the eventCounter increments,” und „your code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes.” Er „lets you filter by model type and transaction author”; das Beispiel der Session übergibt"App"als Author, sodass app-stämmige Änderungen nicht an einen externen Server zurückgespielt werden. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
codable(iOS 27.0 beta). „Uses the property’s codable representation to store the property.” Deklariert alsstatic var codable: Schema.Attribute.Option { get }. ↩↩↩↩↩↩ -
Apple, WWDC26 Session 274, What’s new in SwiftData. Apple stellt
ResultsObservervor, der „fetches data from your SwiftData store and then observes your store for changes”, aber „works anywhere in your app (independent of SwiftUI views) using Swift Observation”, und nennt ein State-Objekt oder ein in SceneKit geschriebenes Spiel als Fälle, die@Querynicht erreichen kann. ↩↩↩ -
Apple Developer Documentation:
fetchCount(_:)undfetchIdentifiers(_:)aufModelContext.fetchCountist deklariert alsfunc fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModelund „returns the number of models that match the criteria of the specified fetch descriptor.”fetchIdentifiersist deklariert alsfunc fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModelund gibt „an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria” zurück; einefetchIdentifiers(_:batchSize:)-Überladung gibt die Identifier in Batches alsFetchResultsCollection<PersistentIdentifier>zurück. Quelle des Musters „betroffene Identifier abrufen und dann mit der View vergleichen”: Paraphrasiert aus einer lokal transkribierten Aufzeichnung des WWDC 2026 SwiftData Group Lab; Apple veröffentlicht keine offiziellen Untertitel für die Labs. ↩↩↩