SwiftDatas wahre Kosten sind Schemadisziplin
Get Bananas’ ShoppingItem ist das kanonische Beispiel dafür, warum SwiftData-Schemadisziplin wichtig ist. Das ursprüngliche Schema enthielt keinen lastModified-Zeitstempel; ihn nachträglich hinzuzufügen erforderte eine spezifische Migrationsform, weil bereits Daten auf der Festplatte vorhanden waren, und das Feld wurde gezielt als optional gestaltet, um einen Migrationsabsturz zu beheben, der auftrat, als es zunächst als nicht-optional hinzugefügt worden war.1
SwiftDatas API besteht aus zwei Makros. @Model an einer Klasse macht sie zu einem persistenten Typ. @Attribute(.unique) an einer Eigenschaft verleiht ihr eine Eindeutigkeitsbeschränkung. Das Framework verbirgt Core Datas Stack-Verwaltung, den Tanz mit Value-Transformern und das NSManagedObjectContext-Boilerplate. Was das Framework jedoch nicht verbirgt, ist die Schemamigration; es macht die Migration lediglich deklarativ statt imperativ. Die Kosten dafür, Migrationen nicht zu beachten, sind der Bug, der die Daten eines Benutzers bei einem Routine-Update auslöscht.
Die These: SwiftData ist günstig zum Einstieg und teuer bei nachlässiger Migration. Die Disziplin liegt in Benennung, Optionalität und VersionedSchema ab Tag eins – nicht ab dem Tag, an dem Sie merken, dass Sie es hätten haben sollen.
TL;DR
- Das
@Model-Makro macht aus einer Klasse einen persistenten SwiftData-Typ. Das Framework generiert das Schema zur Compilezeit aus den Eigenschaftsdeklarationen. - Eine neue optionale Eigenschaft hinzuzufügen ist eine No-Op-Migration: SwiftDatas Lightweight-Migration erledigt das. Eine nicht-optionale Eigenschaft zu einem bestehenden Schema hinzuzufügen erfordert ein
VersionedSchemaplus einenMigrationPlan, der dem Framework mitteilt, wie das neue Feld für bestehende Zeilen zu befüllen ist. - Die Kosten dafür,
VersionedSchemaab Tag eins zu überspringen, bestehen darin, dass jede nicht-triviale v2-Schemaänderung das Risiko birgt, die Datenbank des Benutzers zu verwerfen, weil der Lightweight-Pfad konservativ ist und abbricht, wenn er die Migration nicht ableiten kann. @Attribute(.unique)ist das richtige Werkzeug für natürliche Schlüssel (eine selbstgenerierteUUID, eine importierte externe ID).@Relationshipist das richtige Werkzeug für Eltern-Kind-Referenzen. Beide sind Makros, die unter der Haube die richtige Core-Data-Verkabelung erzeugen.2
Was @Model tatsächlich macht
Ein SwiftData-Typ ist eine Swift-Klasse, auf die das @Model-Makro angewendet wurde. Get Bananas’ ShoppingItem ist die kanonische Form:
import Foundation
import SwiftData
@Model
final class ShoppingItem {
@Attribute(.unique) var id: UUID
var name: String
var amount: String
var section: String
var isChecked: Bool
var isOptional: Bool
var sortOrder: Int
var lastModified: Date?
init(id: UUID = UUID(), name: String, amount: String, section: String,
isOptional: Bool = false, sortOrder: Int = 0) {
self.id = id
self.name = name
self.amount = amount
self.section = section
self.isChecked = false
self.isOptional = isOptional
self.sortOrder = sortOrder
self.lastModified = Date()
}
}
Drei Details zu dieser Form, die die API verbirgt.
@Model erfordert keine separate Schema-Deklaration für den persistenten Speicher. SwiftData liest die Klassendefinition zur Compilezeit und synthetisiert das Schema. Die Eigenschaften der Klasse werden zu den Attributen des Modells; ihre Swift-Typen werden zu den Spaltentypen. Es gibt keine .xcdatamodeld-Datei zu pflegen (obwohl Core Datas zugrundeliegendes NSManagedObjectModel weiterhin existiert und das Schema zur Laufzeit unterstützt).2
@Attribute(.unique) ist eine Beschränkung an einer einzelnen Spalte, keine PRIMARY KEY-Deklaration. SwiftDatas persistente Identität ist der PersistentIdentifier, der pro Zeile automatisch generiert wird. Die Deklaration @Attribute(.unique) teilt dem Framework mit: „Diese Spalte speichert höchstens eine Zeile pro Wert.” Wenn Sie ein Modell mit einem .unique-Wert einfügen, der bereits existiert, führt SwiftData ein Upsert durch: Die bestehende Zeile wird aktualisiert statt zurückgewiesen. Für Produktcode ist die Semantik bedeutsam: .unique ist keine Validierung auf UI-Ebene, die das Einreichen von Duplikaten verhindert; es ist eine Höchstens-eins-Speichergarantie, die still zusammenführt. Das id: UUID-Muster oben ist das empfohlene für die prozessübergreifende Synchronisation (wenn Sie eine stabile Kennung wollen, die das Verschwinden des prozessinternen PersistentIdentifier überlebt), und das Upsert-Verhalten ist genau das, was Sie wollen, wenn dieselbe UUID aus zwei Sync-Pfaden eintrifft.
@Model-Klassen sind Referenztypen, keine Werttypen. Das Mutieren einer Eigenschaft an einer ShoppingItem-Instanz löst SwiftDatas Änderungsverfolgung aus; das Framework registriert die Änderung und persistiert sie beim nächsten Speichern des Kontexts. Die SwiftUI-Integration über @Query rendert jede View neu, die das passende Predicate beobachtet. Das Muster ähnelt @Observable (behandelt in What SwiftUI Is Made Of), mit Persistenz darüber gelegt.
Optionale Felder sind die günstige Migration
Das Feld lastModified: Date? an ShoppingItem ist optional, und die Optionalität ist tragend. Das Feld wurde nach dem Release von v1 hinzugefügt, um geräteübergreifende Synchronisation und Konfliktauflösung zu unterstützen; bestehende Zeilen auf Benutzergeräten hatten keinen lastModified-Wert. Ein optionales Feld ohne Standardwert lässt SwiftDatas Lightweight-Migration die Hinzufügung erledigen, ohne dass Migrationscode geschrieben werden müsste: Bestehende Zeilen erhalten nil; neue Zeilen erhalten, was auch immer der Init setzt.3
Der Lightweight-Migrationspfad ist der höfliche Pfad des Frameworks. SwiftData inspiziert das neue Schema und den persistenten Speicher, leitet die kleinste kompatible Änderung ab und wendet sie an. Die Migration läuft automatisch; der Benutzer sieht nichts; die App startet normal mit den bestehenden Daten. Die Fälle, die der Lightweight-Pfad sauber bewältigt:
- Hinzufügen einer optionalen Eigenschaft
- Entfernen einer Eigenschaft (die Daten werden verworfen; bestehende Lesevorgänge sehen die Spalte nicht mehr)
- Umbenennen eines Attributs, das das Framework anhand eines Hinweises zuordnen kann (mittels
@Attribute(originalName: ...)) - Umbenennen einer
@Model-Klasse, die das Framework zuordnen kann (mittels@Model.originalNameoder eines Hinweises)
Die Fälle, in denen der Lightweight-Pfad abbricht:
- Hinzufügen einer nicht-optionalen Eigenschaft ohne Standardwert zu einem bestehenden Schema (bestehende Zeilen haben keinen Wert, mit dem sie befüllt werden könnten)
- Ändern des Typs einer Eigenschaft (z. B.
Int→String) - Aufteilen eines Modells in zwei Modelle oder Zusammenführen zweier in eines
- Alles, was benutzerdefinierte Logik zur Migration erfordert
Wenn der Lightweight-Pfad abbricht, ist das sichere Verhalten, die Migration scheitern zu lassen. Das unsichere Verhalten wäre, die Datenbank zu verwerfen und neu zu beginnen; das Framework ist konservativ und weigert sich, das stillschweigend zu tun. Der Benutzer sieht den App-Absturz beim Start mit einem Migrationsfehler; der Entwickler sieht einen Stack Trace, der auf die Schemadiskrepanz verweist; niemand verliert Daten, aber alle verlieren das Vertrauen.
Die Kosten dafür, VersionedSchema ab Tag eins zu überspringen, zeigen sich an der Grenze v2 → v3, wenn Sie die dritte Funktion hinzufügen, deren Schemaänderung das übersteigt, was der Lightweight-Pfad bewältigt.
VersionedSchema und MigrationPlan: die Disziplin ab Tag eins
VersionedSchema deklariert eine bestimmte Version des Modellschemas. MigrationPlan deklariert, wie von einer Version zur nächsten migriert wird.4 Die Form:
import SwiftData
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [
SchemaV1.self,
SchemaV2.self,
]
static var stages: [MigrationStage] = [
MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
]
}
Die Modellklassen selbst wandern in den versionierten Schema-Namensraum:
extension SchemaV1 {
@Model
final class ShoppingItemV1 { /* v1 fields */ }
}
extension SchemaV2 {
@Model
final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}
Der ModelContainer wird mit dem Migrationsplan konstruiert:
let container = try ModelContainer(
for: ShoppingItemV2.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration("ShoppingList")
)
Der Migrationsplan gibt dem Framework einen typisierten Graphen, wie sich das Schema entwickelt. Wenn die v2-ausgelieferte App gegen eine v1-Datenbank startet, läuft das Framework den Migrationsplan ab, wendet die benannten Stufen an und bringt die Datenbank auf v2. Wenn Sie v3 ausliefern, fügen Sie SchemaV3.self zu schemas hinzu und eine neue MigrationStage zwischen v2 und v3.
Die Disziplin besteht darin, VersionedSchema bereits in v1 auszuliefern, auch wenn es nur eine Version gibt. Die Kosten dafür sind eine zusätzliche Datei und eine zusätzliche enum-Deklaration. Die Kosten dafür, es nicht zu tun, bestehen darin, dass die erste nicht-triviale Schemaänderung in v2 erfordert, v1 nachträglich in ein VersionedSchema zu wickeln, was machbar ist, aber Sorgfalt erfordert, um die exakte v1-Form zu treffen, damit das Framework die bestehenden Daten als SchemaV1 identifizieren kann. Das zukünftige Sie, das an v2 arbeitet, wird die Steuer zahlen; das gegenwärtige Sie kann sie einmal zahlen und vergessen.
Custom MigrationStage für die schwierigen Fälle
Lightweight-Migrationen decken die meisten additiven Änderungen ab. Typänderungen, Aufteilungen, Zusammenführungen und bedingte Befüllungen benötigen ein MigrationStage.custom:
static var stages: [MigrationStage] = [
MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// Read v1 rows; stage any derived state to a transient store
// (UserDefaults / temp file) since the v1 and v2 contexts do
// not share state, and didMigrate cannot read v1.
let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
stageDerivedState(from: v1Items)
},
didMigrate: { context in
// Populate v2-only fields on existing rows
let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
for item in v2Items where item.lastModified == nil {
item.lastModified = Date()
}
try context.save()
}
)
]
Die beiden Closures feuern vor und nach dem Anwenden der strukturellen Migration durch das Framework. willMigrate läuft gegen das v1-Schema; didMigrate läuft gegen das v2-Schema. Der Closure-Body ist normaler SwiftData-Code (Fetch-Descriptors, Speichern des Modellkontexts, dieselben APIs, die in der laufenden App verwendet werden), der gegen einen transienten In-Migration-Kontext arbeitet.
Das Muster, das die Produktion überlebt, besteht darin, willMigrate leer zu halten und alle Befüllungslogik in didMigrate zu legen. Das Lesen von v1-Daten innerhalb von willMigrate ist erlaubt, aber das v2-Schema existiert aus Sicht des Frameworks noch nicht, sodass jede Berechnung in einen transienten Speicher gestaffelt werden muss, den die didMigrate-Closure lesen kann. Die einfachere Regel: Strukturelle Migrationen sind Aufgabe des Frameworks; das Befüllen von v2-exklusiven Feldern an bestehenden Zeilen ist Aufgabe von didMigrate.
Wann @Attribute und @Relationship ihren Namen verdienen
Zwei Makros erledigen den Großteil der Schemadekoration in @Model-Klassen.
@Attribute dekoriert eine einzelne Eigenschaft mit einer Beschränkung oder einem Hinweis:
@Attribute(.unique)erzwingt Eindeutigkeit, wie beiShoppingItem.id@Attribute(.externalStorage)speichert großeData-Blobs außerhalb der Datenbank (Bilddaten, Audiopuffer)@Attribute(originalName: "old_field_name")ordnet eine Eigenschaft einer umbenannten Spalte während der Migration zu@Attribute(.transformable(by: ...))wendet einenValueTransformerauf einen Nicht-Codable-Typ an
Die richtige Disziplin: Verwenden Sie .unique für Felder, die wirklich eindeutig sein sollen (eine selbstgenerierte UUID, eine externe ID), verwenden Sie .externalStorage für jeden Blob über ein paar KB, verwenden Sie originalName, wenn ein v2-Umbenennen einer Eigenschaft sonst die v1-Daten verlieren würde.
@Relationship dekoriert eine Eigenschaft, die auf eine andere @Model-Klasse oder eine Sammlung davon zeigt:
@Model
final class List {
var name: String
@Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
var items: [ShoppingItem] = []
}
@Model
final class ShoppingItem {
var name: String
var list: List?
}
Das deleteRule: .cascade bedeutet: Das Löschen der übergeordneten List löscht alle untergeordneten ShoppingItem-Zeilen. Der Parameter inverse: teilt dem Framework mit, welche Eigenschaft am Kind zurück auf das Elternteil zeigt; das Framework verwendet ihn für vorhersagbare bidirektionale Pflege. SwiftData kann die Inverse manchmal automatisch ableiten, und inverse: nil wird für explizit unidirektionale Beziehungen unterstützt, aber der sichere Standard ist, inverse: immer dann zu deklarieren, wenn die Ableitung mehrdeutig wäre.5
Die richtige Disziplin: Deklarieren Sie Beziehungen mit explizitem deleteRule (der Standard ist .nullify, was selten das ist, was Sie wollen) und deklarieren Sie inverse: immer dann, wenn die Beziehung bidirektional ist (statt sich auf die Ableitung des Frameworks zu verlassen). Die impliziten Standards sind meist falsch; die explizite Form ist ein zusätzlicher Parameter und ein für immer eingesparter Bug.
Was ich anders bauen würde
Drei Muster, die die Apps im Cluster entweder ausliefern oder sich wünschen, sie hätten sie ausgeliefert.
Liefern Sie VersionedSchema ab v1 aus. Jede ausgelieferte @Model-Klasse sollte ab Tag eins innerhalb eines VersionedSchema leben. Die Kosten betragen ein umhüllendes enum pro Schemaversion. Der Nutzen ist, dass die erste nicht-triviale Änderung in v2 eine Einzeilen-Hinzufügung zu MigrationPlan.schemas ist statt eines zweitägigen rückwirkenden Refactorings.
Machen Sie jeden Zeitstempel optional. Felder wie lastModified, createdAt und updatedAt, die für geräteübergreifende Synchronisation oder Konfliktauflösung existieren, sollten in v1 optional sein, falls das v1-Produkt sie nicht benötigt. Optionalität hält die Migration auf v2 (wenn Sie sie brauchen) günstig. Sie bei bestehenden Zeilen während didMigrate zu befüllen ist eine Schleife; sie ab v1 nicht-optional zu machen ist eine Beschränkung, die das Backfill auf Benutzerdaten brechen kann.
Verwenden Sie UUIDs als natürlichen Schlüssel, nicht den PersistentIdentifier. SwiftDatas PersistentIdentifier ist prozessintern. Geräteübergreifende Synchronisation, MCP-Integration (behandelt in Two Agent Ecosystems, One Shopping List) und jede prozessexterne Referenz benötigen eine stabile Kennung. Eine UUID mit @Attribute(.unique) ist die richtige Form; der prozessinterne PersistentIdentifier ist die falsche Form für alles, was eine Prozessgrenze überschreitet.
Wann @Model die falsche Antwort ist
Drei Fälle, in denen SwiftData nicht das richtige Werkzeug ist:
Schlüssel-Wert-Zustand mit einem einzigen Datensatz. App-Einstellungen, die vom Benutzer gewählte Sprache, der Zeitstempel der letzten Synchronisation. Verwenden Sie UserDefaults oder NSUbiquitousKeyValueStore (behandelt in Five Apple Platforms, Three Shared Files). SwiftDatas Overhead für eine einzelne Zeile ist verschwendete Zeremonie; Schlüssel-Wert-Speicher sind das richtige Substrat.
Server-autoritative Daten ohne Offline-Schreibvorgänge. Eine Liste, die von einer REST-API abgerufen und schreibgeschützt angezeigt wird. SwiftData ist Overkill, wenn die Wahrheit auf dem Server liegt und der lokale Cache nur ein Cache ist. Ein einfacher Codable-Snapshot in Documents/ plus ein im Speicher gecachtes Array reicht aus; die SwiftData-Migrationssteuer lohnt sich nicht, wenn die Daten einen Hard Reset nicht überleben.
Mehrprozess-Koordination. SwiftData operiert innerhalb eines Prozesses. Ein MCP-Server, der außerhalb der iOS-App läuft, kann den SwiftData-Container der App nicht lesen oder schreiben. Prozessübergreifender Zustand benötigt eine andere Form: eine JSON-Datei in iCloud Drive, einen gemeinsamen App-Group-Container oder eine explizite Synchronisationsschicht, die Prozesse überbrückt. (Get Bananas paart SwiftData mit JSON in iCloud Drive aus genau diesem Grund.)6
Die Daten sind große Blobs, die sich selten ändern. Eine 10-MB-Audiodatei, ein 50-MB-Bilddatensatz. Verwenden Sie @Attribute(.externalStorage), falls die Blobs innerhalb von SwiftData-Zeilen liegen; verwenden Sie ansonsten direkt das Dateisystem mit Metadaten in SwiftData, die auf Datei-URLs verweisen.
Was das Muster für Apps bedeutet, die auf iOS 26+ ausgeliefert werden
Drei Kernaussagen.
-
Die Makros sind der einfache Teil. Die Migrationen sind die Kosten.
@Modelund@Attributesind zweizeilige Deklarationen, die viel Core-Data-Verkabelung verbergen. Migrationsdisziplin ist das, wofür Sie über die Lebenszeit der App tatsächlich bezahlen; gestalten Sie v1 mit v2 im Hinterkopf. -
VersionedSchemaab Tag eins ist für ausgelieferte Apps nicht verhandelbar. Das umhüllendeenumist eine zusätzliche Datei. Die rückwirkenden Kosten, es später hinzuzufügen, sind viel höher. -
Optionale Felder und explizite Beziehungen sind die günstige Versicherung. Optionale Zeitstempel für Sync-Metadaten, explizites
deleteRuleundinverse:an Beziehungen. Beides sind winzige Deklarationen, die viel v2-Flexibilität erkaufen.
Der vollständige Apple-Ecosystem-Cluster: typisierte App Intents für Apple Intelligence; MCP-Server für LLM-übergreifende Agenten; die Routing-Frage zwischen ihnen; Foundation Models für On-Device-LLM und das Tool-Protokoll; Live Activities für die Sperrbildschirm-Zustandsmaschine auf iOS; der watchOS-Laufzeitvertrag auf der Apple Watch; SwiftUI-Internals für das Framework-Substrat; RealityKits räumliches Mentalmodell für visionOS-Szenen; Liquid-Glass-Muster für die visuelle Schicht; Multi-Plattform-Auslieferung für geräteübergreifende Reichweite. Der Hub befindet sich in der Apple Ecosystem Series. Für breiteren Kontext zu iOS mit AI-Agenten siehe den iOS Agent Development guide.
FAQ
Was ist der Unterschied zwischen @Model und Core Datas NSManagedObject?
@Model ist ein Swift-Makro, das die NSManagedObject-Verkabelung unter der Haube generiert. SwiftData verwendet Core Data als seinen Backing Store, sodass das Laufzeitmodell dasselbe ist; der Unterschied liegt in der Oberfläche. @Model entfernt die .xcdatamodeld-Datei, die Value-Transformer-Zeremonie und die Lebenszyklusverwaltung des NSManagedObjectContext. Sie erhalten denselben persistenten Speicher mit einer Swift-geformten API.
Brauche ich VersionedSchema, wenn ich nie plane, das Schema zu ändern?
Falls Ihre App möglicherweise eine v2 ausliefert: ja. Falls es eine einmalige Demo ist: nein. Die Kosten für VersionedSchema ab v1 betragen eine zusätzliche enum-Deklaration. Die Kosten dafür, es rückwirkend in v2 hinzuzufügen, bestehen darin, die exakte Form des v1-Schemas zu treffen, damit das Framework bestehende Daten erkennt – machbar, aber fehleranfällig. Die meisten ausgelieferten Apps werden irgendwann eine Schemaänderung benötigen; planen Sie das in v1 ein.
Wann sollte ich @Attribute(.unique) verwenden?
Wenn das Feld ein natürlicher Schlüssel für die Zeile ist: eine selbstgenerierte UUID, eine importierte externe ID, ein zugewiesener Slug. SwiftData behandelt .unique als Upsert: Wenn Sie ein Modell einfügen, dessen .unique-Wert bereits existiert, wird die bestehende Zeile aktualisiert statt eine neue Zeile angehängt. Diese Semantik macht Upsert-artige Sync-Pfade (dieselbe UUID, die von zwei Geräten kommt) sicher; sie ist auch der Grund, warum .unique das falsche Werkzeug für Anzeigename-Felder wie title ist, weil zwei Benutzer, die denselben Titel eingeben, ihre Zeilen still zusammenführen würden, statt zwei eigenständige Datensätze zu erzeugen.
Wie behandle ich ein nicht-optionales Feld, das zu einem bestehenden Schema hinzugefügt wird?
Verwenden Sie ein MigrationStage.custom mit einer didMigrate-Closure, die das Feld an bestehenden Zeilen befüllt. Oder einfacher: Deklarieren Sie das Feld in der neuen Schemaversion als optional und befüllen Sie es träge beim Zugriff. Optionalität ist die günstigere Migration; nicht-optionale Hinzufügungen benötigen explizite Befüllungslogik.
Was ist PersistentIdentifier versus meine eigene UUID?
PersistentIdentifier ist SwiftDatas prozessinterne Zeilen-ID; sie wird automatisch generiert und überlebt die Lebenszeit des laufenden Prozesses. Ihre eigene UUID mit @Attribute(.unique) ist eine stabile prozess- und geräteübergreifende Kennung. Verwenden Sie PersistentIdentifier für prozessinterne Referenzen innerhalb der App. Verwenden Sie eine UUID für alles, was eine Prozessgrenze überschreitet (geräteübergreifende Synchronisation, externe Integrationen, MCP-Tools, Netzwerkaufrufe).
Referenzen
-
Autors Get Bananas, eine SwiftUI-Einkaufslisten-App, die SwiftData mit JSON-Synchronisation über iCloud Drive und einem MCP-Server kombiniert. Das
ShoppingItem-Modell entwickelte sich über den frühen Entwicklungszyklus weiter; das FeldlastModified: Date?wurde nach dem ursprünglichen Schema hinzugefügt (Commit268a00dam 2025-12-01, „Make lastModified optional to fix migration crash”), weil das Feld nicht-optional zu machen die Migration brach, wenn bestehende Zeilen keinen Wert hatten, mit dem sie hätten befüllt werden können. ↩ -
Apple Developer, “SwiftData” und “Adding and editing persistent data in your app”. Das
@Model-Makro, die@Attribute-Constraint-Oberfläche und die Beziehung zu Core DatasNSManagedObjectModel. ↩↩ -
Apple Developer, “Preserving your app’s model data across launches” und “Adopting SwiftData for a Core Data app”. Lightweight-Migrationssemantik und was das Framework zum Abbruch bringt. ↩
-
Apple Developer, “VersionedSchema” und “SchemaMigrationPlan”. Versionierte Schemadeklarationen, Migrationsstufendefinitionen und der
ModelContainer-Konstruktor, der einen Migrationsplan entgegennimmt. ↩ -
Apple Developer, “Defining data relationships with enumerations and model classes” und “Schema.Relationship”. Das
@Relationship-Makro,deleteRule-Optionen (.cascade,.nullify,.deny,.noAction) und die Rolle desinverse:-Parameters bei der Pflege bidirektionaler Beziehungen. ↩ -
Autors Analyse in Two Agent Ecosystems, One Shopping List, 29. April 2026, und Five Apple Platforms, Three Shared Files. Die prozess- und geräteübergreifenden Sync-Muster von Get Bananas + Return, die SwiftData innerhalb eines Mehrprozess-Workflows ergänzen (und manchmal ersetzen). ↩