← Alle Beiträge

SwiftData-Migrationen: leichtgewichtig oder benutzerdefiniert – und wann Sie kein V2 brauchen

Das Schema-Migrationsmodell von SwiftData ist ein struktureller Fortschritt gegenüber Core Data, mit einer Falle, in die Teams immer wieder tappen: Sie deklarieren ein neues VersionedSchema für Änderungen, die SwiftData über Inline-Standardwerte automatisch erledigen würde. Das Ergebnis ist ein Absturz auf dem Gerät mit der Meldung „Duplicate version checksums across stages detected“, obwohl der Code richtig aussah und sauber gebaut wurde. Das tatsächliche Migrationsmodell des Frameworks besteht aus drei Bausteinen (VersionedSchema, MigrationStage, SchemaMigrationPlan) und kennt drei Migrationsarten (automatisch leichtgewichtig, deklariert leichtgewichtig, benutzerdefiniert)1. Die meisten Schemaänderungen laufen automatisch. Manche brauchen eine deklarierte leichtgewichtige Stage. Eine kleine Minderheit braucht eine benutzerdefinierte Stage mit den Closures willMigrate und didMigrate.

Dieser Beitrag geht das Migrationsmodell entlang der Apple-Dokumentation durch, benennt die Fälle, die jede Migrationsart abdeckt, und behandelt die Klassenvererbung von iOS 26 sowie den Stand der Migrationen in den iOS-27-Betas. Der Rahmen lautet: „Was deklariere ich selbst, und was übernimmt SwiftData für mich?“ Denn diese Entscheidung bestimmt, ob die Migration sauber ausgeliefert wird oder beim ersten Start abstürzt. Die begleitende Frage, wie sich ein v1-Schema so entwerfen lässt, dass diese Migrationen günstig bleiben, behandelt Die wahren Kosten von SwiftData sind Schemadisziplin.

Kurzfassung

  • SwiftData-Migrationen setzen sich aus drei Protokollen zusammen: VersionedSchema (eine Momentaufnahme der Modelltypen zu einer Version), MigrationStage (ein einzelner Übergang von fromVersion zu toVersion mit den Fällen .lightweight oder .custom) und SchemaMigrationPlan (die geordnete Liste der Stages)1.
  • Eine neue @Model-Eigenschaft mit Inline-Standardwert (var foo: Bool = false) erfordert kein neues VersionedSchema. SwiftData erledigt die Ergänzung automatisch als leichtgewichtige Migration. Wer dafür ein V2 deklariert, erzeugt Abstürze mit „Duplicate version checksums across stages detected“.
  • Leichtgewichtige Migrationen decken ab: Hinzufügen, Umbenennen und Löschen von Entitäten, Attributen und Beziehungen; Ändern von Beziehungstypen; Deklarieren von @Attribute(originalName:), um Umbenennungen nachzuverfolgen; Festlegen von Löschregeln. Die meisten Schemaänderungen fallen hierunter.
  • Benutzerdefinierte Migrationen (MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:)) decken Datentransformationen ab: das Aufteilen einer Spalte in zwei, das Berechnen abgeleiteter Felder, das Verschieben von Daten zwischen Modellen. willMigrate erhält den alten Kontext, didMigrate den neuen.
  • iOS 26 ergänzt Klassenvererbung für @Model-Typen2. Schemas, die Vererbung übernehmen, springen auf eine neue Version mit einer leichtgewichtigen Stage ausgehend von der vorherigen Version mit flachen Modellen.

Das dreiteilige Modell

Eine SwiftData-Migration setzt sich aus drei Bausteinen zusammen.

VersionedSchema

Eine Momentaufnahme der Modelltypen zu einer bestimmten Schemaversion1. Das Protokoll verlangt:

  • static var versionIdentifier: Schema.Version. Ein semantisches Versionstripel (Schema.Version(1, 0, 0)).
  • static var models: [any PersistentModel.Type]. Das Array der @Model-Typen in dieser Version.
enum SchemaV1: VersionedSchema {
    static let versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Item.self]
    }

    @Model
    final class Item {
        var name: String
        var createdAt: Date
        init(name: String, createdAt: Date) {
            self.name = name
            self.createdAt = createdAt
        }
    }
}

Das Muster mit einem Enum und darin verschachtelten Typen ist die übliche Konvention. Jedes VersionedSchema versieht seine Modellklassen mit einem eigenen Namensraum, sodass mehrere Schemas mit demselben Modellnamen während einer Migration nebeneinander im Code existieren können.

MigrationStage

Ein einzelner Übergang zwischen zwei VersionedSchema-Typen3. Es gibt zwei Fälle:

  • .lightweight(fromVersion: any VersionedSchema.Type, toVersion: any VersionedSchema.Type). Deklariert einen Übergang, den SwiftData ohne App-Code erledigt. Die Parameter sind die VersionedSchema-Typen selbst (etwa SchemaV1.self), nicht rohe Schema.Version-Werte.
  • .custom(fromVersion:toVersion:willMigrate:didMigrate:). Deklariert einen Übergang mit Code, der vor und/oder nach der Datenmigration läuft. Für die Versionsargumente gelten dieselben Parametertypen wie bei .lightweight.

SchemaMigrationPlan

Die geordnete Liste der Stages, die das Schema von einer beliebigen früheren Version auf die aktuelle bringt1.

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )

    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SchemaV2.self,
        toVersion: SchemaV3.self,
        willMigrate: { context in
            // Pre-migration: read old data, prepare it
            try context.save()
        },
        didMigrate: { context in
            // Post-migration: backfill new fields
            let descriptor = FetchDescriptor<SchemaV3.Item>()
            let items = try context.fetch(descriptor)
            for item in items {
                item.computedField = computeFromExisting(item)
            }
            try context.save()
        }
    )
}

Der ModelContainer wird sowohl mit dem aktuellen Schema als auch mit dem Migrationsplan eingerichtet:

let container = try ModelContainer(
    for: SchemaV3.Item.self,
    migrationPlan: AppMigrationPlan.self,
    configurations: ModelConfiguration(...)
)

SwiftData liest beim Erzeugen des Containers die aktuelle Schemaversion des persistenten Speichers, durchläuft die Stages des Plans von dieser Version aufwärts bis zur aktuellen und wendet jede Stage der Reihe nach an.

Was leichtgewichtige Migrationen automatisch erledigen

Die meisten Schemaänderungen brauchen keine benutzerdefinierte Stage1:

  • Ein Attribut mit Standardwert hinzufügen. var foo: Bool = false an einem bestehenden @Model läuft automatisch.
  • Eine neue Entität (Modellklasse) hinzufügen. Neue Typen erscheinen, sobald ihr VersionedSchema das aktuelle ist; bestehende Daten bleiben erhalten.
  • Ein Attribut oder eine Entität entfernen. SwiftData verwirft die Spalte oder die Tabelle.
  • Ein Attribut oder eine Entität umbenennen. Ergänzen Sie @Attribute(originalName: "oldName") an der Eigenschaft, um die Daten zu erhalten; SwiftData bildet alt auf neu ab.
  • Einen Beziehungstyp ändern. Von einfach auf mehrfach, von mehrfach auf mehrfach und so weiter.
  • Löschregeln festlegen. @Relationship(deleteRule: .cascade) und ähnliche Ergänzungen sind leichtgewichtig.

Für Änderungen aus dieser Liste besteht das richtige Muster darin, überhaupt kein neues VersionedSchema zu deklarieren, sofern die Modelltypen ansonsten unverändert bleiben. SwiftData führt die leichtgewichtige Migration automatisch gegen das bestehende Schema aus.

Die Falle: Ein neues Feld erfordert kein V2

Der häufigste Fehler bei SwiftData-Migrationen: Jemand fügt eine neue Eigenschaft mit Inline-Standardwert hinzu (var foo: Bool = false) und deklariert anschließend ein SchemaV2, das auf dieselben Modelltypen verweist wie SchemaV1. Der Build ist sauber. Der erste Start auf einem Gerät mit vorhandenen V1-Daten stürzt mit Duplicate version checksums across stages detected ab, weil sowohl SchemaV1 als auch SchemaV2 dieselbe Prüfsumme ergeben (die Modelltypen haben sich nicht so verändert, dass SwiftData einen Unterschied bemerkt).

Das richtige Muster: Lassen Sie das bestehende VersionedSchema unangetastet, fügen Sie die neue Eigenschaft mit Inline-Standardwert am Modell hinzu und überlassen Sie die Arbeit der automatischen leichtgewichtigen Migration von SwiftData. Kein MigrationPlan, keine MigrationStage, kein V2 nötig.

// V1 schema
enum SchemaV1: VersionedSchema {
    @Model
    final class Item {
        var name: String
        // BEFORE: just these two properties
        var createdAt: Date
        // AFTER: add a third with inline default
        var isFavorite: Bool = false   // Lightweight, automatic
    }
}

Die Änderung var isFavorite: Bool = false geht ohne jede MigrationStage-Deklaration in den Versand. Der ModelContainer-Initialisierer, der kein migrationPlan: übergibt, funktioniert:

let container = try ModelContainer(
    for: SchemaV1.Item.self,
    configurations: ModelConfiguration(...)
)

Ein V2-Schema ist erst dann nötig, wenn eine Änderung nicht leichtgewichtig sein kann (eine Datentransformation, eine Modellaufteilung, ein Umbau der Vererbung, der eigene Logik verlangt). In diesen Fällen ist V2 echt, und ein SchemaMigrationPlan steuert den Übergang.

Wann benutzerdefinierte Migrationen nötig sind

Benutzerdefinierte Migrationen rechtfertigen ihren Aufwand in drei Fällen:

1. Ein Feld auf mehrere aufteilen. Aus einem String-Feld mit dem Inhalt "Last, First" werden zwei Felder, firstName und lastName. Die Migration muss den alten Wert lesen, ihn zerlegen und die neuen Felder schreiben.

static let migrateV1toV2 = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        let descriptor = FetchDescriptor<SchemaV2.Person>()
        let people = try context.fetch(descriptor)
        for person in people {
            let parts = person.fullName.split(separator: ", ", maxSplits: 1)
            person.lastName = String(parts.first ?? "")
            person.firstName = String(parts.dropFirst().first ?? "")
        }
        try context.save()
    }
)

Die Closure didMigrate läuft gegen den Kontext des neuen Schemas, die neuen Felder sind also zugänglich. Das Entfernen des alten fullName muss unter Umständen so lange aufgeschoben werden, bis die neuen Felder gefüllt sind; das Aufräumen erfolgt dann in einer nachgelagerten Stage von V2 auf V3.

2. Abgeleitete Felder berechnen. Ein neues @Attribute, das von bestehenden Daten abhängt, muss zur Migrationszeit nachgefüllt werden.

3. Daten zwischen Modellen verschieben. Eine Umstrukturierung, bei der Daten aus Item zwischen Item und einem neuen Modell Tag aufgeteilt werden, verlangt eigene Logik, um die Tags aus den alten Daten zuzuordnen.

Die allgemeine Regel: leichtgewichtig, wenn sich die Form des Schemas ändert; benutzerdefiniert, wenn sich die Form der Daten ändert.

willMigrate und didMigrate im Vergleich

Benutzerdefinierte Stages haben zwei Closures, die zu unterschiedlichen Zeitpunkten aufgerufen werden4:

willMigrate läuft, bevor SwiftData die Schemamigration anwendet. Der Modellkontext, den die Closure erhält, ist der Kontext des alten Schemas. Nutzen Sie ihn, um Daten zu erfassen, sie zu denormalisieren oder Hilfszustände vorzubereiten, bevor sich das Schema darunter verändert.

didMigrate läuft nach der Schemamigration. Der Modellkontext gehört zum neuen Schema. Nutzen Sie ihn, um neue Felder nachzufüllen, abgeleitete Daten zu berechnen oder die Migration abzuschließen.

Beide Closures dürfen nil sein, wenn sie nicht gebraucht werden. Die meisten benutzerdefinierten Migrationen nutzen nur didMigrate; willMigrate ist dann nützlich, wenn die Migration alte Daten lesen muss, die nach der Schemaänderung nicht mehr erreichbar sind.

Die Closure erhält einen ModelContext und darf abfragen, ändern und speichern. Sie ist als throwing deklariert; Fehler dringen aus der Migration nach außen und brechen sie ab.

iOS 26: Klassenvererbung für @Model

iOS 26 führt Klassenvererbung für SwiftData-Modelle ein2. Modelle können nun Eltern-Kind-Beziehungen haben:

@Model
class Vehicle {
    var make: String
    var year: Int
    init(make: String, year: Int) {
        self.make = make
        self.year = year
    }
}

@Model
final class Car: Vehicle {
    var doorCount: Int
    init(make: String, year: Int, doorCount: Int) {
        self.doorCount = doorCount
        super.init(make: make, year: year)
    }
}

Schemas, die Vererbung übernehmen, springen auf eine neue Version mit einer leichtgewichtigen Migrations-Stage ausgehend von der vorherigen Version mit flachen Modellen. Der Übergang läuft automatisch, sofern die Vererbung die bestehenden Eigenschaften erhält; neue Felder an der Unterklasse folgen dem üblichen Muster mit Inline-Standardwerten.

Das Muster passt zu Fällen, in denen mehrere @Model-Typen Merkmale teilen: ein Elternteil Vehicle mit den Kindern Car, Truck, Motorcycle; ein Elternteil Account mit den Kindern CheckingAccount und SavingsAccount. Die gemeinsamen Eigenschaften liegen am Elternteil, die Besonderheiten an den Kindern.

iOS 27: Das Migrationsmodell bleibt, der Store wird beobachtbar

An der Migrationsmechanik selbst ändern die iOS-27-Betas nichts. VersionedSchema, MigrationStage und SchemaMigrationPlan gelten unverändert weiter, und jedes Muster von oben trifft wortwörtlich zu. Was iOS 27 ergänzt, liegt neben den Migrationen statt in ihnen: eine neue Oberfläche zur „Data store observation“ mit den beiden Typen ResultsObserver und HistoryObserver sowie die Option @Attribute(.codable), die eine Eigenschaft über ihre Codable-Darstellung speichert6.

Zwei dieser Ergänzungen verdienen in einem Migrationsleitfaden Erwähnung.

@Attribute(.codable) senkt den künftigen Migrationsdruck. Ein deklarativ gespeicherter Codable-Werttyp bedeutet weniger Fälle, in denen Sie sonst eine Struktur in parallele Spalten auffalten und später eine benutzerdefinierte Stage schreiben müssten, um sie wieder zusammenzusetzen. Schemas, die die Option an neuen Eigenschaften übernehmen, folgen den weiter oben beschriebenen Regeln für Inline-Standardwerte; es handelt sich um eine Attributoption, nicht um eine Änderung der Schemaform6.

HistoryObserver schließt den Kreis nach einer benutzerdefinierten Migration. Ein Nachfüllen in didMigrate schreibt Zeilen, von denen der Rest der App (und jedes Widget oder jede Erweiterung, die den Store beobachtet) erfahren muss. Unter iOS 27 sieht ein Beobachter, der die persistente Historie über HistoryObserver verfolgt, die Transaktionen der Migration eintreffen und kann ModelContext.fetchHistory aufrufen, um genau die Änderungen zu lesen, gefiltert nach Modelltyp und Transaktionsautor, statt alles neu abzufragen6. Die vollständige Geschichte der Beobachtung behandelt SwiftData in iOS 27: Beobachtung und Historie.

Für die Planung heißt das: Nichts in den 27er-Betas erzwingt einen Sprung der Schemaversion, und kein Migrationscode muss umgeschrieben werden. Übernehmen Sie die neuen Beobachtungstypen dort, wo Ihre Abgleichslogik nach der Migration bisher gepollt oder erneut abgefragt hat.

Migrationen testen

Eine Migration, die kompiliert, ist noch keine Migration, die ausgeliefert werden kann. Drei Testmuster lohnen sich vor jedem Release:

1. Round-Trip-Test mit einer Kopie der Produktivdatenbank. Ziehen Sie eine aktuelle Datenbank in Produktivform (oder erzeugen Sie synthetische V1-Daten über Tests), öffnen Sie sie mit dem V2-fähigen Container und prüfen Sie, ob die Daten korrekt migrieren. Der Test fängt Fehler in benutzerdefinierten Migrationen ab, die der Typprüfer nicht sehen kann.

2. Die alte Version startet weiterhin. Bauen Sie die vorherige App-Version, starten Sie sie einmal, um V1-Daten zu erzeugen, bauen Sie dann die neue Version und prüfen Sie, ob sie ohne Absturz startet. Der Test fängt die Falle „Duplicate version checksums“ und ähnliche Deklarationsfehler ab.

3. Wiederherstellung nach fehlgeschlagener Migration. Was passiert, wenn die Migration einen Fehler wirft? Das Verhalten von SwiftData hängt von der Konfiguration des Containers ab; in Produktiv-Apps darf ein unbehandelter Migrationsfehler keine Nutzerdaten stillschweigend löschen. Testen Sie den Fehlerpfad ausdrücklich und entscheiden Sie, was die App dann tut (zurückrollen, nachfragen, aus einem Backup wiederherstellen).

Der Beitrag über die einzige Quelle der Wahrheit aus demselben Cluster behandelt die verwandte Frage, was geschieht, wenn ein SwiftData-Store durch prozessübergreifende Synchronisierung ersetzt wird. Migrationen sind das lokale Gegenstück dieses Musters.

Migrationen über Prozessgrenzen hinweg ausliefern und den Fortschritt anzeigen

Zwei betriebliche Details, die die Dokumentation nicht in den Vordergrund stellt, die das SwiftData-Team auf der WWDC 2026 aber ausdrücklich benannt hat5: wo Migrationen laufen, wenn eine App Widgets oder Erweiterungen hat, und wie sich eine Fortschritts-UI ansteuern lässt, während eine Migration läuft.

Ein Prozess besitzt die Migration. Widgets und Erweiterungen bekommen nicht dieselben Laufzeitressourcen wie die Haupt-App und können eine Migration deshalb nicht sicher ausführen. Die Empfehlung lautet, den SchemaMigrationPlan1 vollständig aus den Widget- und Erweiterungs-Targets herauszuhalten und niemals von dort aus zu migrieren. Wählen Sie einen Prozess, üblicherweise die Haupt-App, als Eigentümer der Datenbank. Öffnet ein Widget den Container und liegt der Store auf der Platte in einem unversionierten (älteren) Schema, schlägt das Öffnen fehl. Behandeln Sie diesen Fehler als Signal, dass eine Migration ansteht: Zeigen Sie eine Oberfläche, die den Nutzer bittet, die Haupt-App zu öffnen, lassen Sie die App die Migration ausführen und die migrierte Schemaversion in ein gemeinsam genutztes UserDefault schreiben. Das Widget liest diesen Wert beim nächsten Mal und öffnet den Container in genau der Version, auf die die App bereits migriert hat. So bleibt ein einziger Schreiber zuständig, und zwei Prozesse liefern sich kein Rennen um dieselbe Datei.

Der Fortschritt ergibt sich aus der Anzahl der Stages, nicht aus der verstrichenen Zeit. SwiftData bietet keine eigene API für den Migrationsfortschritt5. Um eine Fortschrittsanzeige zu speisen, zählen Sie die Gesamtzahl der benutzerdefinierten Migrations-Stages im Plan und überschreiben den didMigrate-Handler je Stage4, sodass jede Stage ihre Position meldet: „Stage N von M“. Die Zahl spiegelt abgeschlossene Stages wider, nicht verstrichene Zeit; der Balken rückt also in diskreten Schritten vor statt gleichmäßig. Die begleitende Gestaltungsfrage ist, was die App während der Migration zeigt: Ein nackter Spinner wirkt wie ein Hänger, und Nutzer springen ab. Halten Sie die App teilweise nutzbar, wo die Daten das erlauben, oder beschreiben Sie zumindest, was jede Stage ergänzt (welche neuen Funktionen die Migration freischaltet), damit sich das Warten wie ein Fortschritt auf etwas hin anfühlt und nicht wie tote Zeit.

Häufige Fehlerbilder

Drei Muster aus den SwiftData-Fehlerprotokollen:

Ein V2 für eine Änderung deklarieren, die SwiftData automatisch erledigen würde. Der Absturz mit „Duplicate version checksums“. Lösung: Deklarieren Sie für Eigenschaften mit Inline-Standardwert kein neues Schema, sondern überlassen Sie sie SwiftData.

Benutzerdefinierter Migrationscode, der nicht speichert. Eine didMigrate-Closure, die Entitäten ändert, aber context.save() nicht aufruft, erzeugt eine Migration, die einmal läuft, ihre Arbeit verwirft und bei jedem Start erneut anläuft (weil die Migration unabgeschlossen wirkt). Lösung: Jede Closure, die Daten ändert, muss vor der Rückkehr try context.save() aufrufen.

Eine Eigenschaft ohne @Attribute(originalName:) umbenennen. SwiftData behandelt die neue Eigenschaft als neu und die alte als gelöscht; vorhandene Daten der alten Eigenschaft gehen verloren. Lösung: Deklarieren Sie @Attribute(originalName: "oldName") var newName: ..., damit SwiftData die Daten über die Umbenennung hinweg abbildet.

Was dieses Muster für Apps ab iOS 26 bedeutet

Drei Erkenntnisse.

  1. Standardmäßig ohne VersionedSchema-Leiter arbeiten. Eigenschaften mit Inline-Standardwerten hinzufügen, ungenutzte Felder löschen, mit @Attribute(originalName:) umbenennen: alles leichtgewichtig und automatisch. Die VersionedSchema-Leiter ist für Änderungen da, die SwiftData wirklich nicht automatisch bewältigen kann (Datentransformationen, eigene Logik, Umbauten der Vererbung).

  2. MigrationStage.custom für Datentransformationen nutzen, nicht für Änderungen der Schemaform. Die Closures willMigrate und didMigrate sind für Code gedacht, der auf Daten arbeitet, nicht für die Erklärung, dass sich das Schema geändert hat. Änderungen der Schemaform laufen über leichtgewichtige Stages.

  3. Migrationen mit echten V1-Daten testen, nicht nur mit synthetischen Testdaten. Migrationen, die synthetische Round-Trips bestehen, können an Daten in Produktivform mit Sonderfällen dennoch scheitern (nullbare Felder, die das Schema nicht abdeckte, große Datenmengen, die in ein Timeout laufen, und so weiter). Die Kosten des Testens sind gering; die Kosten eines Migrationsabsturzes beim ersten Start sind real.

Das vollständige Cluster zum Apple-Ökosystem: typisierte App Intents; MCP-Server; die Frage nach dem richtigen Weg; Foundation Models; die Unterscheidung zwischen LLM zur Laufzeit und im Werkzeug; drei Oberflächen; das Muster der einzigen Quelle der Wahrheit; Zwei MCP-Server; Hooks für die Apple-Entwicklung; Live Activities; der Laufzeitvertrag von watchOS; SwiftUI von innen; das räumliche Denkmodell von RealityKit; Schemadisziplin in SwiftData; Muster für Liquid Glass; Ausliefern über mehrere Plattformen; die Plattformmatrix; das Vision-Framework; Symbol Effects; Core-ML-Inferenz; die Writing-Tools-API; Swift Testing; das Privacy Manifest im Detail; Barrierefreiheit als Plattformmerkmal; Typografie mit SF Pro; räumliche Muster in visionOS; das Speech-Framework; worüber ich nicht schreibe. Der Knotenpunkt ist die Serie zum Apple-Ökosystem. Für den weiteren Zusammenhang von iOS und KI-Agenten siehe den Leitfaden zur iOS-Agentenentwicklung.

Häufige Fragen

Brauche ich immer einen SchemaMigrationPlan?

Nein. Apps mit einer einzigen Schemaversion (das erste Release oder Apps, die bisher nur leichtgewichtige Änderungen vorgenommen haben) brauchen keinen SchemaMigrationPlan. Der ModelContainer-Initialisierer nimmt die Modelle des Schemas direkt entgegen. Der Parameter migrationPlan: wird erst nötig, sobald zum ersten Mal eine benutzerdefinierte Migrations-Stage deklariert wird (oder sobald man zum ersten Mal eine ausdrückliche Versionsleiter deklarieren will).

Woran erkenne ich, ob meine Änderung leichtgewichtig ist?

Apples Liste der leichtgewichtig möglichen Änderungen1: Entitäten, Attribute und Beziehungen hinzufügen, sie entfernen, mit @Attribute(originalName:) umbenennen, die Kardinalität von Beziehungen ändern, Löschregeln festlegen. Passt die Änderung in eine dieser Kategorien und bleibt die Struktur der Modellklassen ansonsten unverändert, läuft die Migration automatisch und es braucht keine VersionedSchema-Leiter. Verlangt die Änderung eine Datentransformation (berechnen, aufteilen, verschieben), ist sie benutzerdefiniert.

Können willMigrate und didMigrate beide gesetzt werden?

Ja. Beide Closures sind einzeln optional, können aber gemeinsam angegeben werden. willMigrate läuft vor der Migration gegen den Kontext des alten Schemas, didMigrate danach gegen den Kontext des neuen Schemas. Die beiden decken Vorbereitung und Abschluss ab.

Was passiert, wenn eine Migration einen Fehler wirft?

Der Fehler dringt aus der Initialisierung des ModelContainer nach außen. Der Container lässt sich nicht öffnen. Wie sich die App verhält, hängt davon ab, wie der Fehler behandelt wird: Manche Apps zeigen eine Oberfläche zur Wiederherstellung, manche versuchen, aus einem Backup zurückzuspielen, manche löschen den beschädigten Store und beginnen von vorn. SwiftData löscht bei einem Migrationsfehler keine Nutzerdaten stillschweigend; die Behandlung des Fehlers liegt bei der App.

Wie teste ich eine Migration, ohne Produktivdaten zu berühren?

Bauen Sie ein Test-Target, das einen ModelContainer auf eine temporäre Datei-URL richtet, ihn mit V1-Daten füllt und ihn anschließend mit dem neuen Container öffnet, der den Migrationsplan enthält. Prüfen Sie, ob die migrierten Daten den Erwartungen entsprechen. Das Muster funktioniert in Unit- wie in Integrationstests; für möglichst realistische Ergebnisse nehmen Sie eine Kopie einer echten Datenbank in Produktivform.

Funktioniert die Klassenvererbung von iOS 26 mit bestehenden Schemas?

Ja, mit einer leichtgewichtigen Migration. Apps, die Vererbung übernehmen, springen auf eine neue Schemaversion (etwa V4) und deklarieren ein MigrationStage.lightweight(fromVersion: V3.self, toVersion: V4.self). Die flachen Eigenschaften der Elternklasse bleiben erhalten, und die für die Unterklasse spezifischen Eigenschaften kommen mit Inline-Standardwerten hinzu. Die leichtgewichtige Migration von SwiftData bewältigt die strukturelle Änderung.

Quellen


  1. Apple Developer Documentation: Protokollreferenzen zu VersionedSchema und SchemaMigrationPlan. Das Migrationsmodell. Siehe auch den verwandten Leitfaden Adopting SwiftData for a Core Data app für die vollständige Darstellung der Schemaentwicklung. 

  2. Apple Developer: SwiftData: Dive into inheritance and schema migration (WWDC 2025, Session 291). Die Einführung der Klassenvererbung von SwiftData in iOS 26. 

  3. Apple Developer Documentation: MigrationStage mit den Fällen .lightweight(fromVersion:toVersion:) und .custom(fromVersion:toVersion:willMigrate:didMigrate:)

  4. Apple Developer Documentation: MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:) für die Signatur des Falls. Die Semantik, dass willMigrate gegen den alten und didMigrate gegen den neuen Kontext läuft, ist in der WWDC-2025-Session 291 SwiftData: Dive into inheritance and schema migration dokumentiert, derselben Session, die für die Vererbungsergänzung in iOS 26 herangezogen wird. 

  5. WWDC 2026 SwiftData Group Lab (Session 8017). Sinngemäß wiedergegeben nach einer lokal transkribierten Aufzeichnung des SwiftData Group Lab der WWDC 2026; Apple veröffentlicht für die Labs keine offiziellen Untertitel. Die Regelung zu Widgets und Erweiterungen (ein Prozess besitzt die Migration, der Fehlerpfad ist das Migrationssignal, die migrierte Version liegt in einem UserDefault) sowie die Fortschrittstechnik über die Anzahl der Stages (den didMigrate-Handler je Stage überschreiben, um Stage N von M zu melden, da es keine eigene Fortschritts-API gibt) wurden vom SwiftData-Entwicklungspanel beschrieben. Die Symbole SchemaMigrationPlan und didMigrate in MigrationStage.custom sind gegen die in 1 und 4 zitierte Apple-Dokumentation abgesichert; das Fehlen einer eigenen Fortschritts-API gibt die Darstellung des Panels im Lab wieder. 

  6. Apple Developer Documentation: ResultsObserver und HistoryObserver (beide iOS 27.0 Beta, unter dem Thema „Data store observation“ von SwiftData) sowie Schema.Attribute.Option.codable (iOS 27.0 Beta), „uses the property’s codable representation to store the property“. Laut WWDC26-Session 274, What’s new in SwiftData, stellt HistoryObserver einen beobachtbaren eventCounter bereit, der hochzählt, sobald neue Transaktionen eintreffen; der Code reagiert darauf mit einem Aufruf von ModelContext.fetchHistory samt Filtern für Modelltyp und Transaktionsautor. 

Verwandte Beiträge

Die wahren Kosten von SwiftData sind Schema-Disziplin

Die API von SwiftData besteht aus zwei Makros. Die Kosten entstehen nach dem Release. Optionale Felder sind die günstige…

23 Min. Lesezeit

SwiftData in iOS 27: Observation und History

iOS 27 gibt SwiftData echte Änderungsbeobachtung: ResultsObserver, HistoryObserver für die Persistent History und Codabl…

14 Min. Lesezeit

Codex CLI installieren und aktualisieren: Mac, Linux, Windows

Jeder Weg, die OpenAI Codex CLI zu installieren, zu aktualisieren, auf eine Version festzulegen und zu deinstallieren – …

19 Min. Lesezeit