← Wszystkie wpisy

Migracje SwiftData: lekkie czy niestandardowe i kiedy V2 nie jest potrzebne

Sposób, w jaki SwiftData obsługuje migracje schematu, jest strukturalnym krokiem naprzód wobec Core Data, ma jednak pułapkę, w którą zespoły wpadają raz za razem: deklarowanie nowego VersionedSchema dla zmian, które SwiftData obsłużyłby automatycznie dzięki wartościom domyślnym podanym w miejscu deklaracji. Skutkiem jest awaria na urządzeniu z komunikatem „Duplicate version checksums across stages detected”, mimo że kod wyglądał poprawnie i kompilował się bez zastrzeżeń. Rzeczywisty model migracji tego frameworka opiera się na trzech elementach (VersionedSchema, MigrationStage, SchemaMigrationPlan) oraz trzech rodzajach migracji: lekkiej automatycznej, lekkiej zadeklarowanej i niestandardowej1. Większość zmian schematu przebiega automatycznie. Część wymaga zadeklarowanego etapu lekkiego. Niewielka mniejszość wymaga etapu niestandardowego z domknięciami willMigrate i didMigrate.

Ten tekst przechodzi przez model migracji w zestawieniu z dokumentacją Apple, nazywa przypadki obsługiwane przez każdy rodzaj migracji oraz omawia dziedziczenie klas wprowadzone w iOS 26, a także to, w jakim miejscu zostawiają migracje bety iOS 27. Ramą jest pytanie „co deklaruję sam, a co SwiftData robi za mnie”, ponieważ ta właśnie decyzja przesądza o tym, czy migracja trafi do użytkowników bez zgrzytu, czy też aplikacja padnie przy pierwszym uruchomieniu. Pytanie towarzyszące, czyli jak zaprojektować schemat v1, aby takie migracje pozostały tanie, omawia tekst Prawdziwym kosztem SwiftData jest dyscyplina schematu.

W skrócie

  • Migracje SwiftData składają się z trzech protokołów: VersionedSchema (migawka typów modelu w danej wersji), MigrationStage (pojedyncze przejście z fromVersion do toVersion, z przypadkami .lightweight albo .custom) oraz SchemaMigrationPlan (uporządkowana lista etapów)1.
  • Dodanie nowej właściwości @Model z wartością domyślną podaną w miejscu deklaracji (var foo: Bool = false) nie wymaga nowego VersionedSchema. SwiftData obsługuje takie dodanie automatycznie, jako migrację lekką. Zadeklarowanie dla niego wersji V2 kończy się awariami „Duplicate version checksums across stages detected”.
  • Migracje lekkie obsługują: dodawanie, zmianę nazwy i usuwanie encji, atrybutów oraz relacji; zmianę typu relacji; deklarowanie @Attribute(originalName:) w celu śledzenia zmian nazw; określanie reguł usuwania. Większość zmian schematu mieści się w tym zbiorze.
  • Migracje niestandardowe (MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:)) obsługują przekształcenia danych: podział jednej kolumny na dwie, wyliczanie pól pochodnych, przenoszenie danych między modelami. willMigrate dostaje stary kontekst, didMigrate nowy.
  • iOS 26 dodaje dziedziczenie klas dla typów @Model2. Schematy, które przyjmują dziedziczenie, przechodzą na nową wersję z etapem lekkim prowadzącym od poprzedniej wersji o płaskich modelach.

Model złożony z trzech elementów

Migracja SwiftData powstaje z trzech elementów.

VersionedSchema

Migawka typów modelu w konkretnej wersji schematu1. Protokół wymaga:

  • static var versionIdentifier: Schema.Version. Trójczłonowa wersja semantyczna (Schema.Version(1, 0, 0)).
  • static var models: [any PersistentModel.Type]. Tablica typów @Model obecnych w tej wersji.
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
        }
    }
}

Wzorzec z wyliczeniem i zagnieżdżonymi w nim typami to przyjęta konwencja. Każdy VersionedSchema nadaje swoim klasom modelu własną przestrzeń nazw, dzięki czemu podczas migracji kilka schematów o tej samej nazwie modelu może współistnieć w kodzie.

MigrationStage

Pojedyncze przejście między dwoma typami VersionedSchema3. Są dwa przypadki:

  • .lightweight(fromVersion: any VersionedSchema.Type, toVersion: any VersionedSchema.Type). Deklaruje przejście, które SwiftData wykonuje bez kodu aplikacji. Parametrami są same typy VersionedSchema (na przykład SchemaV1.self), a nie surowe wartości Schema.Version.
  • .custom(fromVersion:toVersion:willMigrate:didMigrate:). Deklaruje przejście z kodem uruchamianym przed migracją danych, po niej albo w obu momentach. Argumenty wersji mają te same typy co przy .lightweight.

SchemaMigrationPlan

Uporządkowana lista etapów, która prowadzi schemat od dowolnej wcześniejszej wersji do bieżącej1.

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()
        }
    )
}

ModelContainer konfiguruje się jednocześnie bieżącym schematem i planem migracji:

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

Przy tworzeniu kontenera SwiftData odczytuje bieżącą wersję schematu z trwałego magazynu danych, przechodzi etapy planu od tej wersji w przód aż do bieżącej i stosuje każdy etap po kolei.

Co migracje lekkie obsługują automatycznie

Większość zmian schematu nie wymaga etapu niestandardowego1:

  • Dodanie atrybutu z wartością domyślną. var foo: Bool = false w istniejącym @Model działa automatycznie.
  • Dodanie nowej encji (klasy modelu). Nowe typy pojawiają się wtedy, gdy ich VersionedSchema staje się bieżącym; istniejące dane zostają zachowane.
  • Usunięcie atrybutu lub encji. SwiftData porzuca kolumnę albo tabelę.
  • Zmiana nazwy atrybutu lub encji. Warto dodać do właściwości @Attribute(originalName: "oldName"), aby zachować dane; SwiftData odwzoruje stare na nowe.
  • Zmiana typu relacji. Z jeden-do-wielu na wiele-do-wielu i tak dalej.
  • Określenie reguł usuwania. @Relationship(deleteRule: .cascade) i podobne uzupełnienia są lekkie.

Przy zmianach z tej listy właściwe postępowanie polega na tym, aby w ogóle nie deklarować nowego VersionedSchema, o ile typy modelu poza tym pozostają bez zmian. SwiftData przeprowadza migrację lekką automatycznie wobec istniejącego schematu.

Pułapka: dodanie pola nie wymaga wersji V2

Najczęstszy błąd przy migracjach SwiftData: ktoś dodaje nową właściwość z wartością domyślną w miejscu deklaracji (var foo: Bool = false), a następnie deklaruje SchemaV2, który odwołuje się do tych samych typów modelu co SchemaV1. Kompilacja przebiega bez zarzutu. Pierwsze uruchomienie na urządzeniu z istniejącymi danymi V1 kończy się awarią Duplicate version checksums across stages detected, ponieważ zarówno SchemaV1, jak i SchemaV2 dają tę samą sumę kontrolną (typy modelu nie zmieniły się w sposób, który SwiftData rozpoznaje jako różnicę).

Poprawne postępowanie: należy zostawić istniejący VersionedSchema w spokoju, dodać do modelu nową właściwość z wartością domyślną w miejscu deklaracji i pozwolić, aby zajęła się tym automatyczna migracja lekka SwiftData. Żadnego MigrationPlan, żadnego MigrationStage, żadnej wersji V2.

// 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
    }
}

Zmiana var isFavorite: Bool = false trafia do wydania bez jakiejkolwiek deklaracji MigrationStage. Inicjalizator ModelContainer, który nie przekazuje migrationPlan:, działa:

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

Schemat V2 staje się konieczny dopiero wtedy, gdy zmiana nie może być lekka: przekształcenie danych, podział modelu, przebudowa oparta na dziedziczeniu, która wymaga własnej logiki. W takich przypadkach V2 jest prawdziwe, a przejściem steruje SchemaMigrationPlan.

Kiedy migracje niestandardowe są konieczne

Migracje niestandardowe uzasadniają swoją złożoność w trzech przypadkach.

1. Podział jednego pola na kilka. Pole String zawierające "Last, First" zamienia się w dwa pola, firstName i lastName. Migracja musi odczytać starą wartość, rozłożyć ją na części i zapisać nowe pola.

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()
    }
)

Domknięcie didMigrate działa wobec kontekstu nowego schematu, więc nowe pola są dostępne. Usunięcie starego fullName trzeba czasem odłożyć do chwili, gdy nowe pola zostaną wypełnione; porządki stają się wtedy kolejnym etapem z V2 do V3.

2. Wyliczanie pól pochodnych. Nowy @Attribute, który zależy od istniejących danych, trzeba uzupełnić w czasie migracji.

3. Przenoszenie danych między modelami. Reorganizacja, w której dane z Item rozdziela się między Item a nowy model Tag, wymaga własnej logiki przypisującej etykiety na podstawie starych danych.

Zasada ogólna: lekka, gdy zmienia się kształt schematu; niestandardowa, gdy zmienia się kształt danych.

willMigrate a didMigrate

Etapy niestandardowe mają dwa domknięcia, wywoływane w różnych momentach4.

willMigrate działa przed zastosowaniem migracji schematu przez SwiftData. Kontekst modelu, który trafia do domknięcia, należy do starego schematu. Warto go wykorzystać do przechwycenia danych, ich denormalizacji albo przygotowania stanu pomocniczego, zanim schemat zmieni się pod spodem.

didMigrate działa po migracji schematu. Kontekst modelu należy do nowego schematu. Warto go wykorzystać do uzupełnienia nowych pól, wyliczenia danych pochodnych albo domknięcia całej migracji.

Każde z domknięć może mieć wartość nil, jeśli nie jest potrzebne. Większość migracji niestandardowych korzysta wyłącznie z didMigrate; willMigrate przydaje się wtedy, gdy migracja musi odczytać stare dane, które po zmianie schematu przestaną być dostępne.

Domknięcie otrzymuje ModelContext i może pobierać, modyfikować oraz zapisywać dane. Jest zadeklarowane jako throwing: błędy wydostają się z migracji i przerywają ją.

iOS 26: dziedziczenie klas dla @Model

iOS 26 wprowadza dziedziczenie klas dla modeli SwiftData2. Modele mogą teraz tworzyć relacje rodzic-dziecko:

@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)
    }
}

Schematy, które przyjmują dziedziczenie, przechodzą na nową wersję z lekkim etapem migracji prowadzącym od poprzedniej wersji o płaskich modelach. Przejście odbywa się automatycznie, o ile dziedziczenie zachowuje istniejące właściwości; nowe pola w podklasie podlegają zwykłemu wzorcowi z wartościami domyślnymi w miejscu deklaracji.

Wzorzec pasuje do sytuacji, w których kilka typów @Model dzieli wspólne cechy: rodzic Vehicle z dziećmi Car, Truck i Motorcycle; rodzic Account z dziećmi CheckingAccount i SavingsAccount. Wspólne właściwości mieszkają w rodzicu, a szczegóły w dzieciach.

iOS 27: model migracji zostaje, magazyn staje się obserwowalny

Bety iOS 27 nie zmieniają niczego w samej mechanice migracji. VersionedSchema, MigrationStage i SchemaMigrationPlan przechodzą dalej bez zmian, a każdy powyższy wzorzec obowiązuje dosłownie. To, co iOS 27 dokłada, leży obok migracji, a nie w nich: nowa powierzchnia „Data store observation” z dwoma typami, ResultsObserver i HistoryObserver, oraz opcja @Attribute(.codable), która zapisuje właściwość poprzez jej reprezentację Codable6.

Dwa z tych dodatków zasługują na wzmiankę w przewodniku o migracjach.

@Attribute(.codable) zmniejsza przyszłą presję migracyjną. Typ wartościowy zgodny z Codable, zapisywany deklaratywnie, oznacza mniej sytuacji, w których trzeba by spłaszczyć strukturę do równoległych kolumn, a później napisać etap niestandardowy, aby ją z powrotem złożyć. Schematy, które przyjmują tę opcję w nowych właściwościach, podlegają opisanym wcześniej regułom wartości domyślnych: to opcja atrybutu, a nie zmiana kształtu schematu6.

HistoryObserver domyka pętlę po migracji niestandardowej. Uzupełnianie danych w didMigrate zapisuje wiersze, o których musi się dowiedzieć reszta aplikacji (a także każdy widżet czy rozszerzenie obserwujące magazyn). W iOS 27 obserwator śledzący trwałą historię za pomocą HistoryObserver widzi napływające transakcje migracji i może wywołać ModelContext.fetchHistory, aby odczytać dokładnie to, co się zmieniło, z filtrem po typie modelu i autorze transakcji, zamiast pobierać wszystko od nowa6. Pełną opowieść o obserwacji przedstawia tekst SwiftData w iOS 27: obserwacja i historia.

Wniosek na potrzeby planowania: nic w betach iOS 27 nie wymusza podniesienia wersji schematu i żadnego kodu migracji nie trzeba przepisywać. Nowe typy obserwacji warto przyjąć tam, gdzie logika uzgadniania danych po migracji dotąd odpytywała magazyn albo pobierała wszystko ponownie.

Testowanie migracji

Migracja, która się kompiluje, to jeszcze nie migracja gotowa do wydania. Trzy sposoby testowania warto przejść przed wydaniem.

1. Test w obie strony na kopii bazy produkcyjnej. Należy pobrać świeżą bazę w kształcie produkcyjnym (albo wygenerować syntetyczne dane V1 w testach), otworzyć ją kontenerem świadomym wersji V2 i sprawdzić, czy dane migrują poprawnie. Ten test wychwytuje błędy migracji niestandardowych, których kontroler typów nie zobaczy.

2. Stara wersja nadal się uruchamia. Należy zbudować poprzednią wersję aplikacji, uruchomić ją raz, aby powstały dane V1, następnie zbudować nową wersję i sprawdzić, czy startuje bez awarii. Ten test wychwytuje pułapkę „Duplicate version checksums” i podobne błędy deklaracji.

3. Odzyskiwanie po nieudanej migracji. Co się dzieje, gdy migracja zgłosi błąd? Zachowanie SwiftData zależy od konfiguracji kontenera; w aplikacjach produkcyjnych nieobsłużony błąd migracji nie może po cichu usuwać danych użytkownika. Ścieżkę błędu trzeba przetestować wprost i rozstrzygnąć, co robi aplikacja: wycofuje zmiany, pyta użytkownika albo odtwarza dane z kopii zapasowej.

Tekst Jedno źródło prawdy z tego samego zbioru omawia pokrewne pytanie o to, co się dzieje, gdy magazyn SwiftData zostaje zastąpiony przez synchronizację między procesami. Migracje są lokalnym odpowiednikiem tego wzorca.

Wydawanie migracji między procesami i pokazywanie postępu

Dwa szczegóły eksploatacyjne, których dokumentacja nie wysuwa na pierwszy plan, ale które zespół SwiftData wskazał na WWDC 20265: gdzie działają migracje, gdy aplikacja ma widżety albo rozszerzenia, oraz jak zasilić interfejs postępu w trakcie migracji.

Migrację posiada jeden proces. Widżety i rozszerzenia nie dostają takich zasobów wykonawczych jak aplikacja główna, więc nie mogą bezpiecznie przeprowadzić migracji. Zalecenie brzmi: SchemaMigrationPlan1 należy w całości trzymać poza celami widżetu i rozszerzenia oraz nigdy z nich nie migrować. Właścicielem bazy powinien być jeden proces, zwykle aplikacja główna. Jeśli widżet otworzy kontener, a magazyn na dysku znajduje się w nieoznaczonym wersją (starszym) schemacie, otwarcie kończy się błędem. Ten błąd trzeba potraktować jako sygnał, że potrzebna jest migracja: warto pokazać interfejs, który prosi użytkownika o otwarcie aplikacji głównej, pozwolić aplikacji przeprowadzić migrację, a następnie zapisać zmigrowaną wersję schematu we współdzielonym UserDefault. Widżet odczyta tę wartość następnym razem i otworzy kontener w wersji, do której aplikacja już zmigrowała. Ten wzorzec utrzymuje jednego zapisującego u steru i zapobiega wyścigowi dwóch procesów o rozwój tego samego pliku.

Postęp wylicza się z liczby etapów, a nie z upływu czasu. SwiftData nie udostępnia żadnego dedykowanego API postępu migracji5. Aby zasilić wskaźnik postępu, należy policzyć wszystkie niestandardowe etapy migracji w planie i nadpisać obsługę didMigrate w każdym etapie4, tak by każdy zgłaszał swoją pozycję: „etap N z M”. Liczba odzwierciedla etapy ukończone, a nie czas, który upłynął, więc pasek przesuwa się skokowo, a nie płynnie. Towarzyszy temu decyzja projektowa: co aplikacja pokazuje w trakcie migracji. Samo kręcące się kółko czyta się jak zawieszenie i użytkownicy odchodzą. Warto zostawić aplikację częściowo używalną tam, gdzie pozwalają na to dane, albo przynajmniej opisać, co dokłada każdy etap (jakie nowe funkcje odblokowuje migracja), aby czekanie czytało się jak postęp ku czemuś, a nie jak martwy czas.

Typowe tryby awarii

Trzy wzorce z dzienników błędów SwiftData.

Deklarowanie V2 dla zmiany, którą SwiftData obsłużyłby automatycznie. Awaria „Duplicate version checksums”. Rozwiązanie: nie należy deklarować nowego schematu dla dodania właściwości z wartością domyślną; niech zajmie się nimi SwiftData.

Kod migracji niestandardowej, który nie zapisuje. Domknięcie didMigrate, które modyfikuje encje, ale nie wywołuje context.save(), tworzy migrację uruchamianą raz, porzucającą swoją pracę i ponawianą przy każdym starcie (bo wygląda na niedokończoną). Rozwiązanie: każde domknięcie modyfikujące dane musi przed zakończeniem wywołać try context.save().

Zmiana nazwy właściwości bez @Attribute(originalName:). SwiftData traktuje nową właściwość jako nową, a starą jako usuniętą; dane zapisane w starej właściwości przepadają. Rozwiązanie: należy zadeklarować @Attribute(originalName: "oldName") var newName: ..., aby SwiftData przeprowadził dane przez zmianę nazwy.

Co ten wzorzec oznacza dla aplikacji na iOS 26 i nowsze

Trzy wnioski.

  1. Domyślnie bez drabiny VersionedSchema. Dodawanie właściwości z wartościami domyślnymi, usuwanie nieużywanych pól, zmiana nazw przez @Attribute(originalName:): wszystko to jest lekkie i automatyczne. Drabina VersionedSchema służy zmianom, z którymi SwiftData naprawdę nie poradzi sobie sam (przekształcenia danych, własna logika, przebudowy oparte na dziedziczeniu).

  2. MigrationStage.custom służy przekształceniom danych, a nie zmianom kształtu schematu. Domknięcia willMigrate i didMigrate są dla kodu operującego na danych, a nie dla deklarowania, że schemat się zmienił. Zmiany kształtu schematu idą przez etapy lekkie.

  3. Migracje należy testować na prawdziwych danych V1, nie tylko na syntetycznych. Migracje przechodzące syntetyczne testy w obie strony wciąż potrafią polec na danych w kształcie produkcyjnym i ich przypadkach brzegowych: pola dopuszczające wartość pustą, których schemat nie przewidywał, wielkie zbiory wchodzące w limit czasu i tak dalej. Koszt testowania jest niewielki; koszt awarii migracji przy pierwszym uruchomieniu jest całkiem realny.

Pełny zbiór poświęcony ekosystemowi Apple: typowane App Intents; serwery MCP; pytanie o kierowanie żądań; Foundation Models; rozróżnienie między LLM wykonawczym a narzędziowym; trzy powierzchnie; wzorzec jednego źródła prawdy; Dwa serwery MCP; hooki w programowaniu na platformy Apple; Live Activities; kontrakt wykonawczy watchOS; z czego zrobione jest SwiftUI; przestrzenny model myślowy RealityKit; dyscyplina schematu w SwiftData; wzorce Liquid Glass; wydawanie na wiele platform; macierz platform; framework Vision; Symbol Effects; wnioskowanie z Core ML; API Writing Tools; Swift Testing; Privacy Manifest od podszewki; dostępność jako funkcja platformy; typografia SF Pro; wzorce przestrzenne visionOS; framework Speech; o czym odmawiam pisać. Węzłem całości jest seria o ekosystemie Apple. Szerszy kontekst iOS w połączeniu z agentami AI opisuje przewodnik po tworzeniu agentów na iOS.

Często zadawane pytania

Czy SchemaMigrationPlan jest zawsze potrzebny?

Nie. Aplikacje z jedną wersją schematu (pierwsze wydanie albo aplikacje, które dotąd wprowadzały wyłącznie zmiany lekkie) nie potrzebują SchemaMigrationPlan. Inicjalizator ModelContainer przyjmuje modele schematu wprost. Parametr migrationPlan: staje się konieczny przy pierwszym zadeklarowaniu niestandardowego etapu migracji (albo wtedy, gdy po raz pierwszy chce się zadeklarować jawną drabinę wersji).

Po czym poznać, że zmiana jest lekka?

Lista zmian kwalifikujących się jako lekkie według Apple1: dodawanie encji, atrybutów i relacji, ich usuwanie, zmiana nazw przy użyciu @Attribute(originalName:), zmiana liczności relacji, określanie reguł usuwania. Jeśli zmiana mieści się w jednym z tych przypadków, a struktura klas modelu poza tym pozostaje bez zmian, migracja przebiega automatycznie i drabina VersionedSchema nie jest potrzebna. Jeśli zmiana wymaga przekształcenia danych (wyliczenia, podziału, przeniesienia), jest niestandardowa.

Czy willMigrate i didMigrate można ustawić jednocześnie?

Tak. Każde z domknięć jest z osobna opcjonalne, ale można podać oba. willMigrate działa wobec kontekstu starego schematu przed migracją, didMigrate wobec kontekstu nowego schematu po niej. Razem obejmują przygotowanie i zakończenie.

Co się stanie, gdy migracja zgłosi błąd?

Błąd wydostaje się z inicjalizacji ModelContainer. Kontener nie otwiera się. Zachowanie aplikacji zależy od tego, jak obsłużono błąd: jedne aplikacje wyświetlają interfejs odzyskiwania, inne próbują odtworzyć dane z kopii zapasowej, jeszcze inne usuwają uszkodzony magazyn i zaczynają od nowa. SwiftData nie usuwa po cichu danych użytkownika przy nieudanej migracji; obsługa awarii należy do aplikacji.

Jak przetestować migrację bez naruszania danych produkcyjnych?

Należy zbudować cel testowy, który tworzy ModelContainer wskazujący na tymczasowy adres URL pliku, wypełnia go danymi V1, a następnie otwiera nowym kontenerem zawierającym plan migracji. Potem trzeba sprawdzić, czy zmigrowane dane odpowiadają oczekiwaniom. Wzorzec sprawdza się zarówno w testach jednostkowych, jak i integracyjnych; najbardziej realistyczne wyniki daje kopia prawdziwej bazy w kształcie produkcyjnym.

Czy dziedziczenie klas z iOS 26 działa z istniejącymi schematami?

Tak, przy migracji lekkiej. Aplikacje, które przyjmują dziedziczenie, przechodzą na nową wersję schematu (na przykład V4) i deklarują MigrationStage.lightweight(fromVersion: V3.self, toVersion: V4.self). Płaskie właściwości klasy nadrzędnej pozostają, a właściwości charakterystyczne dla podklasy dochodzą z wartościami domyślnymi w miejscu deklaracji. Migracja lekka SwiftData przyjmuje tę zmianę strukturalną.

Źródła


  1. Dokumentacja Apple Developer: opisy protokołów VersionedSchema i SchemaMigrationPlan. Model migracji. Zobacz także pokrewny przewodnik Adopting SwiftData for a Core Data app, zawierający pełną opowieść o ewolucji schematu. 

  2. Apple Developer: SwiftData: Dive into inheritance and schema migration (WWDC 2025, sesja 291). Wprowadzenie dziedziczenia klas SwiftData w iOS 26. 

  3. Dokumentacja Apple Developer: MigrationStage z przypadkami .lightweight(fromVersion:toVersion:) oraz .custom(fromVersion:toVersion:willMigrate:didMigrate:)

  4. Dokumentacja Apple Developer: MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:) — sygnatura tego przypadku. Semantyka, zgodnie z którą willMigrate działa wobec starego kontekstu, a didMigrate wobec nowego, jest udokumentowana w sesji 291 z WWDC 2025 SwiftData: Dive into inheritance and schema migration — tej samej, do której odsyła opis dziedziczenia w iOS 26. 

  5. SwiftData Group Lab na WWDC 2026 (sesja 8017). Parafraza lokalnie przepisanego nagrania SwiftData Group Lab z WWDC 2026; Apple nie publikuje oficjalnych napisów do sesji typu lab. Zasady dotyczące migracji w widżetach i rozszerzeniach (migrację posiada jeden proces, ścieżka błędu jest sygnałem migracji, zmigrowana wersja trafia do UserDefault) oraz technika pokazywania postępu na podstawie liczby etapów (nadpisanie obsługi didMigrate w każdym etapie, aby zgłaszała etap N z M, skoro nie ma dedykowanego API postępu) zostały opisane przez panel inżynierów SwiftData. Symbole SchemaMigrationPlan oraz didMigrate w MigrationStage.custom potwierdza dokumentacja Apple Developer przywołana w 1 i 4; brak dedykowanego API postępu oddaje sposób, w jaki ujął to sam panel podczas sesji. 

  6. Dokumentacja Apple Developer: ResultsObserver i HistoryObserver (oba w becie iOS 27.0, w temacie „Data store observation” w SwiftData) oraz Schema.Attribute.Option.codable (beta iOS 27.0), „uses the property’s codable representation to store the property”. Zgodnie z sesją 274 z WWDC26, What’s new in SwiftData, HistoryObserver udostępnia obserwowalny licznik eventCounter, który rośnie wraz z napływem nowych transakcji, a kod reaguje wywołaniem ModelContext.fetchHistory z filtrami po typie modelu i autorze transakcji. 

Powiązane artykuły

Prawdziwym kosztem SwiftData jest dyscyplina schematu

API SwiftData to dwa makra. Koszt pojawia się po wydaniu aplikacji. Pola opcjonalne to tania migracja; dodanie pola nieo…

16 min czytania

SwiftData w iOS 27: Observation i historia

iOS 27 daje SwiftData obserwację zmian przez ResultsObserver, trwałą historię przez HistoryObserver i codable'owe przech…

10 min czytania

Instalacja i aktualizacja Codex CLI: Mac, Linux, Windows

Każdy sposób instalacji, aktualizacji, przypinania wersji i odinstalowania OpenAI Codex CLI -- skrypt instalacyjny, npm,…

14 min czytania