Die wahren Kosten von SwiftData liegen in der Schema-Disziplin
Genre: shipped-code. Dieser Beitrag dokumentiert die SwiftData-Schema-Entscheidungen aus Get Bananas, Return und Reps: drei Apps, in denen das Schema entweder eine saubere Migration überstanden oder einen Tribut für die fehlende Migrationsplanung gezahlt hat. Der ShoppingItem aus Get Bananas ist das kanonische Beispiel. Das ursprüngliche Schema enthielt keinen lastModified-Zeitstempel; das spätere Hinzufügen erforderte eine spezifische Migrationsform, weil bereits Daten auf der Festplatte vorhanden waren, und das Feld wurde gezielt optional gemacht, um einen Migrations-Crash zu beheben, der beim ersten Hinzufügen als nicht-optionales Feld auftrat.1
Die API von SwiftData besteht aus zwei Macros. @Model an einer Klasse macht diese zu einem persistenten Typ. @Attribute(.unique) an einer Eigenschaft verleiht ihr eine Eindeutigkeitsbedingung. Das Framework verbirgt das Stack-Management von Core Data, den Tanz mit Value-Transformern und den NSManagedObjectContext-Boilerplate. Was das Framework jedoch nicht verbirgt, ist die Schema-Migration; es macht die Migration lediglich deklarativ statt imperativ. Der Preis für mangelnde Aufmerksamkeit bei Migrationen ist der Bug, der bei einem Routine-Update die Daten eines Benutzers auslöscht.
Die These: SwiftData ist günstig im Einstieg und teuer in der schlampigen Migration. Die Disziplin liegt in Benennung, Optionalität und VersionedSchema ab Tag eins – nicht erst an dem Tag, an dem Sie merken, dass Sie es hätten tun sollen.
TL;DR
- Das
@Model-Macro verwandelt eine Klasse in einen persistenten SwiftData-Typ. Das Framework generiert das Schema zur Compile-Zeit aus den Property-Deklarationen. - Das Hinzufügen einer neuen optionalen Property ist eine No-op-Migration: Die leichtgewichtige Migration von SwiftData erledigt das. Das Hinzufügen einer nicht-optionalen Property zu einem bestehenden Schema erfordert ein
VersionedSchemaplus einenMigrationPlan, der dem Framework mitteilt, wie das neue Feld für bestehende Zeilen zu befüllen ist. - Der Preis für das Auslassen von
VersionedSchemaab Tag eins besteht darin, dass jede nicht-triviale v2-Schema-Änderung das Risiko birgt, die Datenbank eines Benutzers zu verwerfen, weil der leichtgewichtige Pfad konservativ ist und aussteigt, sobald er die Migration nicht ableiten kann. @Attribute(.unique)ist das richtige Werkzeug für natürliche Schlüssel (eine selbst generierteUUID, eine importierte externe ID).@Relationshipist das richtige Werkzeug für Eltern-Kind-Referenzen. Beide sind Macros, die unter der Haube die richtige Core-Data-Verkabelung generieren.2
Was @Model tatsächlich macht
Ein SwiftData-Typ ist eine Swift-Klasse mit dem @Model-Macro. Der ShoppingItem aus Get Bananas zeigt 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 an dieser Form, die die API verbirgt.
@Model benötigt keine separate Schema-Deklaration für den persistenten Speicher. SwiftData liest die Klassendefinition zur Compile-Zeit und synthetisiert das Schema. Die Properties der Klasse werden zu den Attributen des Modells; ihre Swift-Typen werden zu den Spaltentypen. Es gibt keine .xcdatamodeld-Datei zu pflegen (auch wenn das zugrunde liegende NSManagedObjectModel von Core Data weiterhin existiert und das Schema zur Laufzeit untermauert).2
@Attribute(.unique) ist eine Bedingung an einer einzelnen Spalte, keine PRIMARY KEY-Deklaration. Die persistente Identität von SwiftData ist der PersistentIdentifier, der automatisch pro Zeile generiert wird. Die @Attribute(.unique)-Deklaration teilt dem Framework mit: „Diese Spalte speichert höchstens eine Zeile pro Wert.” Wenn Sie ein Modell mit einem bereits existierenden .unique-Wert einfügen, führt SwiftData ein Upsert durch: Die bestehende Zeile wird aktualisiert statt abgewiesen. Diese Semantik ist für Produktcode entscheidend: .unique ist keine UI-Validierung, die das Einreichen von Duplikaten verhindert; es ist eine Speichergarantie nach dem Höchstens-Eins-Prinzip, die stillschweigend zusammenführt. Das oben gezeigte Muster id: UUID ist das empfohlene Muster für die prozessübergreifende Synchronisation (wenn Sie einen stabilen Identifier wollen, der das Verschwinden des prozessinternen PersistentIdentifier überdauert), 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 Verändern einer Property an einer ShoppingItem-Instanz löst die Änderungsverfolgung von SwiftData aus; das Framework registriert die Änderung und persistiert sie beim nächsten Kontext-Save. Die SwiftUI-Integration über @Query rendert jede View neu, die das passende Prädikat beobachtet. Das Muster ähnelt @Observable (behandelt in What SwiftUI Is Made Of), nur mit zusätzlich aufgesetzter Persistenz.
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 Cross-Device-Sync und Konfliktauflösung zu unterstützen; bestehende Zeilen auf Benutzergeräten hatten keinen lastModified-Wert. Ein optionales Feld ohne Default ermöglicht es der leichtgewichtigen Migration von SwiftData, das Hinzufügen ohne Migrationscode zu bewältigen: Bestehende Zeilen erhalten nil; neue Zeilen erhalten das, was der Initializer setzt.3
Der leichtgewichtige 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 davon; die App startet normal mit den bestehenden Daten. Folgende Fälle bewältigt der leichtgewichtige Pfad sauber:
- Hinzufügen einer optionalen Property
- Entfernen einer Property (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)
Folgende Fälle veranlassen den leichtgewichtigen Pfad zum Aussteigen:
- Hinzufügen einer nicht-optionalen Property ohne Default zu einem bestehenden Schema (bestehende Zeilen haben keinen Wert zum Befüllen)
- Ändern des Typs einer Property (z. B.
Int→String) - Aufspalten eines Modells in zwei Modelle oder Zusammenführen zweier Modelle in eines
- Alles, was benutzerdefinierte Migrationslogik erfordert
Wenn der leichtgewichtige Pfad aussteigt, 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-Crash beim Start mit einem Migrationsfehler; der Entwickler sieht einen Stack-Trace, der auf das Schema-Mismatch verweist; niemand verliert Daten, aber alle verlieren das Vertrauen.
Der Preis für das Auslassen von VersionedSchema ab Tag eins zeigt sich an der Grenze v2 → v3, wenn Sie das dritte Feature hinzufügen, dessen Schema-Änderung das überschreitet, was der leichtgewichtige Pfad bewältigt.
VersionedSchema und MigrationPlan: die Disziplin ab Tag eins
VersionedSchema deklariert eine spezifische Version des Modell-Schemas. MigrationPlan deklariert, wie von einer Version zur nächsten zu migrieren ist.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 darüber, wie sich das Schema entwickelt. Wenn die v2-App gegen eine v1-Datenbank startet, durchläuft das Framework den Migrationsplan, wendet die benannten Stufen an und bringt die Datenbank auf v2. Wenn Sie v3 ausliefern, fügen Sie SchemaV3.self zu schemas und eine neue MigrationStage zwischen v2 und v3 hinzu.
Die Disziplin besteht darin, VersionedSchema bereits in v1 auszuliefern, auch wenn es nur eine Version gibt. Der Preis dafür ist eine zusätzliche Datei und eine zusätzliche enum-Deklaration. Der Preis dafür, es nicht zu tun, besteht darin, dass die erste nicht-triviale Schema-Änderung in v2 ein nachträgliches Einwickeln von v1 in ein VersionedSchema erfordert, was machbar ist, aber Sorgfalt verlangt, damit das Framework die bestehenden Daten anhand der exakten v1-Form als SchemaV1 identifizieren kann. Das Zukunfts-Ich, das an v2 arbeitet, wird den Tribut zahlen; das Gegenwarts-Ich kann ihn einmalig zahlen und vergessen.
Custom MigrationStage für die schwierigen Fälle
Leichtgewichtige Migrationen decken die meisten additiven Änderungen ab. Typänderungen, Aufspaltungen, 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 wie in der laufenden App), der gegen einen transienten In-Migration-Kontext arbeitet.
Das Muster, das in der Produktion überlebt, lautet: willMigrate leer halten und die gesamte Befüllungslogik in didMigrate packen. Das Lesen von v1-Daten innerhalb von willMigrate ist erlaubt, aber das v2-Schema existiert aus Sicht des Frameworks noch nicht, sodass jegliche Berechnung in einen transienten Speicher gestaged werden muss, den die didMigrate-Closure lesen kann. Die einfachere Regel: Strukturelle Migrationen sind die Aufgabe des Frameworks; das Befüllen von v2-spezifischen Feldern an bestehenden Zeilen ist die Aufgabe von didMigrate.
Wann sich @Attribute und @Relationship ihren Namen verdienen
Zwei Macros leisten den Großteil der Schema-Dekorationsarbeit in @Model-Klassen.
@Attribute dekoriert eine einzelne Property mit einer Bedingung oder einem Hinweis:
@Attribute(.unique)erzwingt Eindeutigkeit, wie beiShoppingItem.id@Attribute(.externalStorage)speichert großeData-Blobs außerhalb der Datenbank (Bilddaten, Audio-Buffer)@Attribute(originalName: "old_field_name")ordnet eine Property 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 sollten (eine selbst generierte UUID, eine externe ID), verwenden Sie .externalStorage für jeden Blob über wenigen KB, verwenden Sie originalName, wenn ein v2-Umbenennen einer Property sonst die v1-Daten verlieren würde.
@Relationship dekoriert eine Property, die auf eine andere @Model-Klasse oder eine Sammlung davon verweist:
@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?
}
deleteRule: .cascade bedeutet, dass das Löschen der übergeordneten List alle untergeordneten ShoppingItem-Zeilen löscht. Der Parameter inverse: teilt dem Framework mit, welche Property am Kind zurück zum Elternteil zeigt; das Framework nutzt ihn für eine berechenbare bidirektionale Pflege. SwiftData kann die Inverse manchmal automatisch ableiten, und inverse: nil wird für explizit unidirektionale Beziehungen unterstützt, aber der sichere Default ist, inverse: immer dann zu deklarieren, wenn die Ableitung mehrdeutig wäre.5
Die richtige Disziplin: Beziehungen mit explizitem deleteRule deklarieren (der Default ist .nullify, was selten das Gewünschte ist) und inverse: deklarieren, wann immer die Beziehung bidirektional ist (statt sich auf die Ableitung des Frameworks zu verlassen). Die impliziten Defaults sind meist falsch; die explizite Form ist ein zusätzlicher Parameter und ein für immer vermiedener Bug.
Was ich anders bauen würde
Drei Muster, die die Apps im Cluster entweder ausliefern oder im Nachhinein gerne ausgeliefert hätten.
VersionedSchema ab v1 ausliefern. Jede ausgelieferte @Model-Klasse sollte ab Tag eins in einem VersionedSchema leben. Der Preis ist eine umhüllende enum pro Schema-Version. Der Nutzen besteht darin, dass die erste nicht-triviale Änderung in v2 eine einzeilige Ergänzung an MigrationPlan.schemas ist statt eines zweitägigen nachträglichen Refactorings.
Jeden Zeitstempel optional machen. Felder wie lastModified, createdAt und updatedAt, die für Cross-Device-Sync oder Konfliktauflösung existieren, sollten in v1 optional sein, wenn das v1-Produkt sie nicht braucht. Optionalität hält die Migration auf v2 (sobald Sie sie brauchen) günstig. Sie an bestehenden Zeilen während didMigrate zu befüllen, ist eine Schleife; sie ab v1 nicht-optional zu machen, ist eine Bedingung, die das Backfill auf Benutzerdaten zerschießen kann.
UUIDs als natürlichen Schlüssel verwenden, nicht den PersistentIdentifier. Der PersistentIdentifier von SwiftData ist prozessintern. Cross-Device-Sync, MCP-Integration (behandelt in Two Agent Ecosystems, One Shopping List) und jede prozessübergreifende Referenz brauchen einen stabilen Identifier. 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 nur einem Datensatz. App-Einstellungen, die ausgewählte Sprache des Benutzers, der Zeitstempel der letzten Synchronisation. Verwenden Sie UserDefaults oder NSUbiquitousKeyValueStore (behandelt in Five Apple Platforms, Three Shared Files). Der Overhead von SwiftData für eine einzelne Zeile ist verschwendete Zeremonie; Schlüssel/Wert-Stores sind das richtige Substrat.
Server-autoritative Daten ohne Offline-Schreibvorgänge. Eine Liste, die von einer REST-API abgerufen und nur lesbar angezeigt wird. SwiftData ist Overkill, wenn die Quelle der Wahrheit der Server ist und der lokale Cache nur ein Cache ist. Ein einfacher Codable-Snapshot in Documents/ plus ein speicherinterner Cache-Array reicht; der SwiftData-Migrationstribut lohnt sich nicht, wenn die Daten einen Hard Reset nicht überleben müssen.
Mehrprozess-Koordination. SwiftData arbeitet innerhalb eines Prozesses. Ein MCP-Server, der außerhalb der iOS-App läuft, kann den SwiftData-Container der App nicht lesen oder beschreiben. Prozessübergreifender Zustand braucht eine andere Form: eine JSON-Datei in iCloud Drive, einen geteilten App-Group-Container oder eine explizite Synchronisationsschicht, die Prozesse überbrückt. (Get Bananas paart SwiftData genau aus diesem Grund mit JSON in iCloud Drive.)6
Die Daten sind große Blobs, die sich selten ändern. Eine 10MB-Audiodatei, ein 50MB-Bilddatensatz. Verwenden Sie @Attribute(.externalStorage), wenn die Blobs innerhalb von SwiftData-Zeilen liegen; andernfalls nutzen Sie das Dateisystem direkt mit Metadaten in SwiftData, die auf Datei-URLs zeigen.
Was das Muster für Apps bedeutet, die auf iOS 26+ ausliefern
Drei Erkenntnisse.
-
Die Macros sind der einfache Teil. Die Migrationen sind die Kosten.
@Modelund@Attributesind zweizeilige Deklarationen, die viel Core-Data-Verkabelung verbergen. Migrationsdisziplin ist das, was Sie über die Lebensdauer der App tatsächlich bezahlen; entwerfen Sie v1 mit v2 im Hinterkopf. -
VersionedSchemaab Tag eins ist für ausgelieferte Apps nicht verhandelbar. Die umhüllendeenumist eine zusätzliche Datei. Die nachträglichen Kosten, sie später hinzuzufügen, sind erheblich 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 Lock-Screen-Zustandsmaschine auf iOS; der watchOS-Runtime-Vertrag auf der Apple Watch; SwiftUI-Internals für das Framework-Substrat; RealityKits räumliches Mental Model für visionOS-Szenen; Liquid-Glass-Muster für die visuelle Schicht; Multi-Plattform-Auslieferung für die geräteübergreifende Reichweite. Der Hub befindet sich unter Apple Ecosystem Series. Für einen 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-Macro, das die NSManagedObject-Verkabelung unter der Haube generiert. SwiftData verwendet Core Data als seinen Backing Store, das Laufzeit-Modell ist also dasselbe; der Unterschied liegt in der Oberfläche. @Model entfernt die .xcdatamodeld-Datei, die Value-Transformer-Zeremonie und das Lifecycle-Management 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 sich um ein einmaliges Demo handelt, nein. Der Preis für VersionedSchema ab v1 ist eine zusätzliche enum-Deklaration. Der Preis dafür, es nachträglich in v2 hinzuzufügen, besteht darin, die exakte v1-Schema-Form zu treffen, damit das Framework bestehende Daten erkennt – machbar, aber fehleranfällig. Die meisten ausgelieferten Apps werden irgendwann eine Schema-Änderung brauchen; 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 selbst generierte 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 anzuhängen. Diese Semantik macht Upsert-artige Sync-Pfade sicher (dieselbe UUID, die von zwei Geräten kommt); sie ist auch der Grund, warum .unique an Anzeigename-Feldern wie title das falsche Werkzeug ist, weil zwei Benutzer, die denselben Titel eintippen, ihre Zeilen stillschweigend zusammenführen würden, statt zwei distinkte Datensätze zu produzieren.
Wie behandle ich ein nicht-optionales Feld, das 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 Schema-Version als optional und befüllen Sie es träge beim Zugriff. Optionalität ist die günstigere Migration; nicht-optionale Ergänzungen brauchen explizite Befüllungslogik.
Was ist PersistentIdentifier im Vergleich zu meiner eigenen UUID?
Der PersistentIdentifier ist die prozessinterne Zeilen-ID von SwiftData; er wird automatisch generiert und überlebt die Lebensdauer des laufenden Prozesses. Ihre eigene UUID mit @Attribute(.unique) ist ein stabiler prozess- und geräteübergreifender Identifier. Verwenden Sie PersistentIdentifier für prozessinterne Referenzen innerhalb der App. Verwenden Sie eine UUID für alles, was eine Prozessgrenze überschreitet (Cross-Device-Sync, externe Integrationen, MCP-Tools, Netzwerkaufrufe).
Quellen
-
Das eigene Get Bananas des Autors, eine SwiftUI-Einkaufslisten-App, die SwiftData mit JSON-Sync via iCloud Drive und einem MCP-Server kombiniert. Das
ShoppingItem-Modell entwickelte sich im frühen Entwicklungszyklus; das FeldlastModified: Date?wurde nach dem ursprünglichen Schema hinzugefügt (Commit268a00dam 2025-12-01, „Make lastModified optional to fix migration crash”), weil das Nicht-Optional-Machen die Migration zerbrach, sobald bestehende Zeilen keinen Wert zum Befüllen hatten. ↩ -
Apple Developer, “SwiftData” und “Adding and editing persistent data in your app”. Das
@Model-Macro, die Bedingungs-Oberfläche von@Attributeund die Beziehung zumNSManagedObjectModelvon Core Data. ↩↩ -
Apple Developer, “Preserving your app’s model data across launches” und “Adopting SwiftData for a Core Data app”. Semantik der leichtgewichtigen Migration und was das Framework zum Aussteigen veranlasst. ↩
-
Apple Developer, “VersionedSchema” und “SchemaMigrationPlan”. Versionierte Schema-Deklarationen, Migrationsstufen-Definitionen und der
ModelContainer-Konstruktor, der einen Migrationsplan annimmt. ↩ -
Apple Developer, “Defining data relationships with enumerations and model classes” und “Schema.Relationship”. Das
@Relationship-Macro, diedeleteRule-Optionen (.cascade,.nullify,.deny,.noAction) und die Rolle desinverse:-Parameters in der bidirektionalen Beziehungspflege. ↩ -
Analyse des Autors in Two Agent Ecosystems, One Shopping List, 29. April 2026, und Five Apple Platforms, Three Shared Files. Die Cross-Process- und Cross-Device-Sync-Muster von Get Bananas + Return, die SwiftData innerhalb eines Multi-Prozess-Workflows ergänzen (und manchmal ersetzen). ↩