SwiftData od podstaw — kompletny przewodnik po persystencji danych w Swift

Kompletny przewodnik po SwiftData — frameworku Apple do persystencji danych w Swift. Od modeli z @Model, przez zapytania @Query, relacje i operacje w tle z @ModelActor, po migracje schematów. Praktyczne przykłady kodu gotowe do użycia.

Wprowadzenie — dlaczego SwiftData zmienia zasady gry

Przez ponad dekadę Core Data był jedynym oficjalnym rozwiązaniem do persystencji danych w ekosystemie Apple. I bądźmy szczerzy — działał solidnie, ale wymagał sporej dawki boilerplate'u. Pliki .xcdatamodeld, podklasy NSManagedObject, ręczne zarządzanie kontekstem i żmudne konfigurowanie stacku. Dla wielu programistów (zwłaszcza tych zaczynających przygodę z iOS) był po prostu za trudny na start.

Na WWDC 2023 Apple pokazało SwiftData — nowy framework do persystencji, zbudowany od podstaw z myślą o nowoczesnym Swifcie i SwiftUI. Zamiast plików XML i edytora modeli definiujesz schemat bezpośrednio w kodzie Swift za pomocą makr. Zamiast NSFetchRequest — używasz deklaratywnego @Query. A integracja ze SwiftUI? Tak naturalna, że widoki odświeżają się automatycznie przy każdej zmianie danych.

Od premiery SwiftData przeszło sporą ewolucję. W iOS 18 pojawiły się ograniczenia unikalności (#Unique), indeksy (#Index), śledzenie historii zmian i własne magazyny danych. W iOS 26 (Xcode 26) doszło dziedziczenie modeli oraz szereg poprawek stabilności. Framework dojrzał na tyle, że naprawdę nadaje się do produkcyjnych aplikacji.

W tym artykule przejdziemy przez SwiftData od zera do poziomu zaawansowanego — od definiowania modeli, przez zapytania i relacje, aż po operacje w tle z @ModelActor, migracje schematów i optymalizację wydajności. Wszystko z działającymi przykładami kodu, gotowymi do wrzucenia do Twojego projektu.

Wymagania i konfiguracja projektu

SwiftData wymaga iOS 17+ (lub macOS 14+, watchOS 10+, tvOS 17+) i Xcode 15 lub nowszego. Jeśli chcesz korzystać z nowszych funkcji jak #Unique czy #Index, potrzebujesz iOS 18+ i Xcode 16+. Najnowsze rzeczy, takie jak dziedziczenie modeli, wymagają już iOS 26 i Xcode 26.

Dobra wiadomość — nie musisz ciągnąć żadnych zewnętrznych zależności. SwiftData jest częścią SDK Apple, więc po prostu importujesz go w pliku źródłowym:

import SwiftData
import SwiftUI

Tworzenie nowego projektu ze SwiftData

W Xcode, tworząc nowy projekt, zaznacz opcję Storage: SwiftData. Xcode wygeneruje za Ciebie podstawową konfigurację z kontenerem modelu. Jeśli dodajesz SwiftData do istniejącego projektu, musisz samodzielnie skonfigurować ModelContainer — pokażemy to za chwilę.

Definiowanie modeli — makro @Model

Sercem SwiftData jest makro @Model. Wystarczy oznaczyć nim klasę Swift, a framework sam ogarnie persystencję — bez żadnych plików XML, bez edytora modeli danych. Serio, to takie proste.

Najprostszy model

import SwiftData

@Model
class Zadanie {
    var tytul: String
    var opis: String
    var ukonczone: Bool
    var dataUtworzenia: Date

    init(tytul: String, opis: String = "", ukonczone: Bool = false) {
        self.tytul = tytul
        self.opis = opis
        self.ukonczone = ukonczone
        self.dataUtworzenia = Date()
    }
}

I to wszystko. Żadnego NSManagedObject, żadnego pliku .xcdatamodeld. Makro @Model generuje conformance do protokołu PersistentModel, obsługę śledzenia zmian i serializacji do SQLite. Jeśli kiedykolwiek walczyłeś z konfiguracją Core Data, to właśnie poczułeś ulgę.

Obsługiwane typy właściwości

SwiftData radzi sobie z większością standardowych typów Swift:

  • Typy proste: String, Int, Double, Bool, Float
  • Typy Foundation: Date, UUID, URL, Data
  • Kolekcje: Array, Dictionary (z typami Codable)
  • Enumy: o ile są Codable i mają RawValue
  • Struktury: o ile są Codable
  • Relacje: inne typy oznaczone @Model

Dostosowywanie właściwości za pomocą @Attribute

Makro @Attribute daje Ci precyzyjną kontrolę nad sposobem przechowywania właściwości:

@Model
class Uzytkownik {
    var imie: String
    var nazwisko: String

    @Attribute(.unique)
    var email: String

    @Attribute(.externalStorage)
    var zdjecie: Data?

    @Attribute(.spotlight)
    var bio: String?

    @Attribute(.encrypt)
    var haslo: String

    init(imie: String, nazwisko: String, email: String) {
        self.imie = imie
        self.nazwisko = nazwisko
        self.email = email
        self.haslo = ""
    }
}

Najważniejsze opcje @Attribute:

  • .unique — gwarantuje unikalność wartości w bazie danych
  • .externalStorage — duże dane (np. obrazki) trafiają poza główny plik SQLite
  • .spotlight — włącza indeksowanie w Spotlight
  • .encrypt — szyfruje dane na dysku
  • .transformable(by:) — używa niestandardowego transformera

Ograniczenia unikalności z #Unique (iOS 18+)

Od iOS 18 możesz definiować złożone ograniczenia unikalności za pomocą makra #Unique:

@Model
class Produkt {
    #Unique<Produkt>([\.nazwaKategorii, \.kodProduktu])

    var nazwaKategorii: String
    var kodProduktu: String
    var cena: Double

    init(nazwaKategorii: String, kodProduktu: String, cena: Double) {
        self.nazwaKategorii = nazwaKategorii
        self.kodProduktu = kodProduktu
        self.cena = cena
    }
}

Gdy próbujesz wstawić duplikat, SwiftData automatycznie robi upsert — aktualizuje istniejący rekord zamiast tworzyć nowy. To ogromne ułatwienie przy synchronizacji danych z serwera (wcześniej trzeba było pisać to ręcznie).

Indeksowanie z #Index (iOS 18+)

Żeby przyspieszyć często wykonywane zapytania, możesz użyć makra #Index:

@Model
class Artykul {
    #Index<Artykul>([\.datapublikacji], [\.kategoria, \.datapublikacji])

    var tytul: String
    var tresc: String
    var kategoria: String
    var datapublikacji: Date

    init(tytul: String, tresc: String, kategoria: String) {
        self.tytul = tytul
        self.tresc = tresc
        self.kategoria = kategoria
        self.datapublikacji = Date()
    }
}

Indeksy działają na poziomie SQLite i potrafią drastycznie przyspieszyć filtrowanie i sortowanie na dużych zbiorach danych. Warto dodawać je od razu tam, gdzie wiesz, że będziesz dużo filtrować.

ModelContainer i ModelContext — silnik SwiftData

Żeby SwiftData zaczął działać, potrzebujesz dwóch rzeczy: ModelContainer (kontener definiujący schemat i konfigurację magazynu) oraz ModelContext (kontekst do odczytu i zapisu danych). Brzmi groźnie, ale w praktyce konfiguracja jest banalna.

Konfiguracja kontenera w SwiftUI

Najprostszy sposób to jedna linijka z modyfikatorem .modelContainer(for:) w głównym widoku aplikacji:

@main
struct MojaAplikacjaApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Zadanie.self, Uzytkownik.self])
    }
}

SwiftData sam tworzy plik SQLite w katalogu danych aplikacji, konfiguruje schemat i udostępnia kontekst w środowisku SwiftUI. Nie musisz się o nic martwić.

Zaawansowana konfiguracja kontenera

Jeśli potrzebujesz większej kontroli — np. konfiguracji CloudKit, własnego katalogu zapisu czy wyłączenia automatycznego zapisu — tworzysz kontener ręcznie:

@main
struct MojaAplikacjaApp: App {
    let kontener: ModelContainer

    init() {
        let schemat = Schema([Zadanie.self, Uzytkownik.self])
        let konfiguracja = ModelConfiguration(
            "MojaBaza",
            schema: schemat,
            isStoredInMemoryOnly: false,
            allowsSave: true,
            groupContainer: .automatic,
            cloudKitDatabase: .private("iCloud.com.moja.aplikacja")
        )

        do {
            kontener = try ModelContainer(
                for: schemat,
                configurations: [konfiguracja]
            )
        } catch {
            fatalError("Nie udało się utworzyć kontenera: \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(kontener)
    }
}

ModelContext — operacje CRUD

W widokach SwiftUI kontekst jest dostępny przez środowisko. Dodawanie i usuwanie danych to dosłownie kilka linii:

struct ContentView: View {
    @Environment(\.modelContext) private var kontekst

    func dodajZadanie() {
        let noweZadanie = Zadanie(tytul: "Naucz się SwiftData")
        kontekst.insert(noweZadanie)
        // Zapis odbywa się automatycznie!
    }

    func usunZadanie(_ zadanie: Zadanie) {
        kontekst.delete(zadanie)
    }
}

Ważna uwaga: SwiftData automatycznie zapisuje zmiany w odpowiedzi na zdarzenia systemowe — np. przejście aplikacji do tła czy nawigację między widokami. Nie musisz ręcznie wywoływać save(), chyba że potrzebujesz natychmiastowego zapisu (np. przed wysłaniem danych na serwer). To faktycznie spora zmiana w porównaniu z Core Data.

Zapytania — @Query i FetchDescriptor

SwiftData daje Ci dwa sposoby pobierania danych: deklaratywne @Query (idealne w widokach SwiftUI) oraz imperatywne FetchDescriptor (świetne w logice biznesowej i operacjach w tle).

@Query — deklaratywne zapytania w SwiftUI

Makro @Query automatycznie pobiera dane i odświeża widok przy każdej zmianie. Szczerze? To chyba moja ulubiona funkcja SwiftData:

struct ListaZadan: View {
    @Query(
        filter: #Predicate<Zadanie> { !$0.ukonczone },
        sort: \Zadanie.dataUtworzenia,
        order: .reverse
    )
    private var aktywneZadania: [Zadanie]

    var body: some View {
        List(aktywneZadania) { zadanie in
            Text(zadanie.tytul)
        }
    }
}

Dynamiczne sortowanie i filtrowanie

Tu jest pewien haczyk. @Query nie obsługuje bezpośrednio dynamicznej zmiany sortowania w runtime. Żeby to obejść, musisz przenieść @Query do podwidoku i przekazywać parametry sortowania przez inicjalizator:

struct ListaZadanFiltrowana: View {
    @Query private var zadania: [Zadanie]

    init(sortujPo: SortDescriptor<Zadanie>, tylkoAktywne: Bool) {
        let predykat: Predicate<Zadanie>? = tylkoAktywne
            ? #Predicate { !$0.ukonczone }
            : nil

        _zadania = Query(
            filter: predykat,
            sort: [sortujPo]
        )
    }

    var body: some View {
        List(zadania) { zadanie in
            Text(zadanie.tytul)
        }
    }
}

// Użycie w widoku nadrzędnym:
struct ContentView: View {
    @State private var sortowanieRosnace = true

    var body: some View {
        ListaZadanFiltrowana(
            sortujPo: SortDescriptor(
                \Zadanie.dataUtworzenia,
                order: sortowanieRosnace ? .forward : .reverse
            ),
            tylkoAktywne: true
        )
    }
}

Trochę więcej kodu niż byśmy chcieli, ale działa niezawodnie. Mam nadzieję, że Apple uprości to w przyszłych wersjach.

FetchDescriptor — zapytania poza SwiftUI

W logice biznesowej lub operacjach w tle sięgaj po FetchDescriptor:

func pobierzOstatnieZadania(kontekst: ModelContext, limit: Int = 10) throws -> [Zadanie] {
    var deskryptor = FetchDescriptor<Zadanie>(
        predicate: #Predicate { !$0.ukonczone },
        sortBy: [SortDescriptor(\Zadanie.dataUtworzenia, order: .reverse)]
    )
    deskryptor.fetchLimit = limit
    return try kontekst.fetch(deskryptor)
}

// Zliczanie rekordów — bez ładowania obiektów do pamięci
func policzAktywneZadania(kontekst: ModelContext) throws -> Int {
    let deskryptor = FetchDescriptor<Zadanie>(
        predicate: #Predicate { !$0.ukonczone }
    )
    return try kontekst.fetchCount(deskryptor)
}

Pro tip: Jeśli potrzebujesz tylko liczbę rekordów, zawsze używaj fetchCount() zamiast pobierania tablicy i sprawdzania .count. Różnica wydajności przy dużych zbiorach danych jest kolosalna — fetchCount() wykonuje SELECT COUNT(*) na poziomie SQLite, bez ładowania czegokolwiek do pamięci.

Relacje między modelami

SwiftData automatycznie wykrywa relacje na podstawie typów właściwości. Jeśli klasa oznaczona @Model ma właściwość będącą inną klasą @Model, framework tworzy relację sam z siebie. Nie musisz niczego konfigurować ręcznie (choć możesz).

Relacja jeden-do-wielu

@Model
class Projekt {
    var nazwa: String
    var opis: String

    @Relationship(deleteRule: .cascade)
    var zadania: [Zadanie] = []

    init(nazwa: String, opis: String = "") {
        self.nazwa = nazwa
        self.opis = opis
    }
}

@Model
class Zadanie {
    var tytul: String
    var ukonczone: Bool

    var projekt: Projekt?

    init(tytul: String, ukonczone: Bool = false) {
        self.tytul = tytul
        self.ukonczone = ukonczone
    }
}

Reguły usuwania

Makro @Relationship pozwala określić, co stanie się z powiązanymi obiektami po usunięciu rodzica:

  • .nullify (domyślna) — właściwość w powiązanych obiektach ustawiana na nil
  • .cascade — usunięcie rodzica automatycznie usuwa wszystkie dzieci
  • .deny — blokuje usunięcie rodzica, jeśli ma powiązane obiekty
  • .noAction — nie robi nic (ostrożnie z tym, bo łatwo zostawić osierocone rekordy)

Uwaga na kolejność elementów w relacjach

To znany problem, który potrafi zaskoczyć. SwiftData nie gwarantuje zachowania kolejności elementów w tablicach relacji. Po ponownym załadowaniu modelu z dysku kolejność elementów może się zmienić. Jeśli kolejność jest dla Ciebie istotna, dodaj właściwość sortOrder: Int do modelu potomnego i sortuj wyniki ręcznie. Trochę upierdliwe, ale na razie nie ma lepszego rozwiązania.

Operacje w tle z @ModelActor

Domyślnie SwiftData działa na głównym wątku (MainActor). To sprawdza się świetnie przy małych zbiorach danych, ale przy importowaniu tysięcy rekordów lub synchronizacji z API — interfejs zamiera. Użytkownik widzi zamrożoną aplikację i zaczyna nerwowo tapać w ekran. Rozwiązanie? @ModelActor.

Tworzenie aktora do operacji w tle

import SwiftData

@ModelActor
actor MenedzerDanych {
    func importujZadania(_ dane: [ZadanieDTO]) throws {
        for dto in dane {
            let zadanie = Zadanie(tytul: dto.tytul)
            zadanie.opis = dto.opis
            modelContext.insert(zadanie)
        }
        try modelContext.save()
    }

    func usunUkonczone() throws {
        let deskryptor = FetchDescriptor<Zadanie>(
            predicate: #Predicate { $0.ukonczone }
        )
        let ukonczone = try modelContext.fetch(deskryptor)
        for zadanie in ukonczone {
            modelContext.delete(zadanie)
        }
        try modelContext.save()
    }
}

Makro @ModelActor automatycznie tworzy dedykowany ModelContext z własnym seryjnym executorem. Operacje wykonują się sekwencyjnie na prywatnej kolejce, nie blokując głównego wątku. Bezpieczeństwo wątkowe out of the box.

Wywoływanie aktora z widoku

struct ContentView: View {
    @Environment(\.modelContext) private var kontekst

    var body: some View {
        Button("Importuj dane") {
            Task.detached {
                let menedzer = MenedzerDanych(
                    modelContainer: kontekst.container
                )
                try await menedzer.importujZadania(daneZSerwera)
            }
        }
    }
}

Przekazywanie danych między aktorami

I tu dochodzimy do ważnego punktu. Modele SwiftData nie są Sendable — nie możesz ich po prostu przekazywać między aktorami. Masz dwa wyjścia:

1. PersistentIdentifier — przekaż identyfikator i pobierz obiekt w docelowym kontekście:

@ModelActor
actor MenedzerDanych {
    func oznaczJakoUkonczone(id: PersistentIdentifier) throws {
        guard let zadanie = modelContext[id] as Zadanie? else { return }
        zadanie.ukonczone = true
        try modelContext.save()
    }
}

2. DTO (Data Transfer Object) — zdefiniuj prostą strukturę Sendable do przenoszenia danych:

struct ZadanieDTO: Sendable {
    let id: UUID
    let tytul: String
    let opis: String
    let ukonczone: Bool
}

@ModelActor
actor MenedzerDanych {
    func pobierzWszystkie() throws -> [ZadanieDTO] {
        let zadania = try modelContext.fetch(FetchDescriptor<Zadanie>())
        return zadania.map { ZadanieDTO(
            id: $0.id, tytul: $0.tytul,
            opis: $0.opis, ukonczone: $0.ukonczone
        )}
    }
}

Osobiście częściej korzystam z podejścia DTO — jest bardziej jawne i łatwiejsze w debugowaniu. PersistentIdentifier jest za to wygodniejsze, gdy potrzebujesz zmodyfikować istniejący obiekt.

Migracje schematów

Modele danych ewoluują z czasem — dodajesz właściwości, zmieniasz typy, usuwasz pola. To normalne. SwiftData obsługuje dwa rodzaje migracji: lekkie (automatyczne) i złożone (ręczne).

Lekkie migracje

SwiftData automatycznie ogarnia następujące zmiany bez żadnej dodatkowej konfiguracji:

  • Dodanie nowej właściwości (z wartością domyślną)
  • Usunięcie istniejącej właściwości
  • Zmiana nazwy właściwości (z użyciem @Attribute(originalName:))
  • Dodanie atrybutów .unique, .externalStorage
// Zmiana nazwy właściwości z zachowaniem danych
@Model
class Zadanie {
    @Attribute(originalName: "tytul")
    var nazwa: String  // Przemianowane z "tytul" na "nazwa"

    var opis: String
    var ukonczone: Bool
    var priorytet: Int = 0  // Nowa właściwość z wartością domyślną
}

Złożone migracje z VersionedSchema

Gdy zmiana wymaga transformacji danych (np. dodanie ograniczenia unikalności do kolumny, w której już są duplikaty), musisz zdefiniować wersjonowane schematy i plan migracji. To trochę więcej roboty, ale daje pełną kontrolę:

// Wersja 1 schematu
enum SchematV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Zadanie.self]
    }

    @Model
    class Zadanie {
        var tytul: String
        var ukonczone: Bool
        init(tytul: String) {
            self.tytul = tytul
            self.ukonczone = false
        }
    }
}

// Wersja 2 — dodajemy priorytet i datę
enum SchematV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Zadanie.self]
    }

    @Model
    class Zadanie {
        var tytul: String
        var ukonczone: Bool
        var priorytet: Int
        var dataUtworzenia: Date

        init(tytul: String) {
            self.tytul = tytul
            self.ukonczone = false
            self.priorytet = 0
            self.dataUtworzenia = Date()
        }
    }
}

// Plan migracji
enum PlanMigracji: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchematV1.self, SchematV2.self]
    }

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

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SchematV1.self,
        toVersion: SchematV2.self
    ) { kontekst in
        // Transformacja danych
        let zadania = try kontekst.fetch(FetchDescriptor<SchematV2.Zadanie>())
        for zadanie in zadania {
            zadanie.priorytet = 0
            zadanie.dataUtworzenia = Date()
        }
        try kontekst.save()
    }
}

Następnie przekaż plan migracji do kontenera:

let kontener = try ModelContainer(
    for: SchematV2.Zadanie.self,
    migrationPlan: PlanMigracji.self
)

Fajne jest to, że SwiftData sam wykona wszystkie potrzebne migracje w odpowiedniej kolejności — nawet jeśli użytkownik przeskakuje z wersji 1 bezpośrednio na wersję 5. Nie musisz się martwić o pośrednie kroki.

Optymalizacja wydajności — praktyczne wskazówki

SwiftData jest całkiem wydajny od razu po wyjęciu z pudełka, ale przy dużych zbiorach danych (myślę tu o tysiącach rekordów) warto znać kilka sztuczek.

1. Filtruj w predykacie, nie w Swift

To chyba najczęstszy błąd, jaki widzę u początkujących. Predykaty tłumaczone są na zapytania SQL — filtrowanie odbywa się na poziomie bazy danych, co jest wielokrotnie szybsze niż ładowanie wszystkiego do pamięci:

// Dobrze — filtrowanie na poziomie bazy
let deskryptor = FetchDescriptor<Zadanie>(
    predicate: #Predicate { $0.priorytet > 3 && !$0.ukonczone }
)
let wyniki = try kontekst.fetch(deskryptor)

// Źle — ładowanie wszystkiego i filtrowanie w pamięci
let wszystkie = try kontekst.fetch(FetchDescriptor<Zadanie>())
let wyniki = wszystkie.filter { $0.priorytet > 3 && !$0.ukonczone }

2. Optymalizuj kolejność warunków w predykatach

Umieszczaj najbardziej restrykcyjne warunki na początku i preferuj szybkie porównania (np. Int) przed wolniejszymi (np. String). Niewielka zmiana, która potrafi przyspieszyć zapytania na dużych tabelach.

3. Używaj fetchLimit i fetchOffset do paginacji

var deskryptor = FetchDescriptor<Zadanie>(
    sortBy: [SortDescriptor(\Zadanie.dataUtworzenia, order: .reverse)]
)
deskryptor.fetchLimit = 20
deskryptor.fetchOffset = 0  // Strona 1

let strona1 = try kontekst.fetch(deskryptor)

deskryptor.fetchOffset = 20  // Strona 2
let strona2 = try kontekst.fetch(deskryptor)

4. Prefetch relacji, gdy wiesz, że będą potrzebne

Relacje w SwiftData ładowane są leniwie (lazy loading). To zazwyczaj dobrze, ale jeśli wiesz, że będziesz potrzebować powiązanych obiektów od razu, użyj prefetching — w przeciwnym razie wpadniesz w klasyczny problem N+1:

var deskryptor = FetchDescriptor<Projekt>()
deskryptor.relationshipKeyPathsForPrefetching = [\.zadania]
let projekty = try kontekst.fetch(deskryptor)

Bez prefetching każdy dostęp do projekt.zadania wyzwala osobne zapytanie SQL. Przy 50 projektach to 50 dodatkowych zapytań. Z prefetching — jedno.

5. Sortuj w SwiftData, nie w Swift

Tak samo jak filtrowanie — sortowanie na poziomie bazy danych jest zawsze szybsze niż sortowanie tablicy w pamięci. Używaj SortDescriptor w FetchDescriptor zamiast .sorted() w Swift.

SwiftData w podglądach Xcode (Previews)

Integracja z Xcode Previews to jedno z tych ułatwień, które naprawdę doceniasz na co dzień. Jedna kluczowa zasada — zawsze używaj kontenera in-memory w podglądach, żeby nie zaśmiecać prawdziwej bazy danych:

#Preview {
    let konfiguracja = ModelConfiguration(isStoredInMemoryOnly: true)
    let kontener = try! ModelContainer(
        for: Zadanie.self,
        configurations: konfiguracja
    )

    // Wstaw przykładowe dane
    let kontekst = kontener.mainContext
    kontekst.insert(Zadanie(tytul: "Przykładowe zadanie"))
    kontekst.insert(Zadanie(tytul: "Drugie zadanie", ukonczone: true))

    return ListaZadan()
        .modelContainer(kontener)
}

Debugowanie SwiftData

Żeby zobaczyć zapytania SQL generowane przez SwiftData, dodaj argument uruchomienia w Xcode:

  1. Otwórz schemat uruchamiania (Edit Scheme → Run → Arguments)
  2. Dodaj argument: -com.apple.CoreData.SQLDebug 1
  3. Dla pełnych szczegółów (włącznie z planem zapytań): -com.apple.CoreData.SQLDebug 3

W konsoli zobaczysz dokładne zapytania SQL — dzięki temu możesz zweryfikować, czy predykaty i indeksy działają tak jak powinny. Nie lekceważ tego narzędzia, potrafi zaoszczędzić godziny debugowania.

Najczęstsze pułapki i jak ich unikać

Na koniec — zbiór najczęstszych błędów, na które natkniesz się pracując ze SwiftData. Lepiej wiedzieć o nich wcześniej niż debugować je o trzeciej w nocy:

  • Force-unwrapping optionali w predykatach — powoduje ciche niepowodzenie zapytania. Zawsze sprawdzaj opcjonalność porównaniem z nil.
  • Przekazywanie modeli między aktorami — modele SwiftData nie są Sendable. Używaj PersistentIdentifier lub DTO (opisałem to wyżej).
  • Sortowanie za pomocą Comparable — konformancja modelu do Comparable nie działa w SortDescriptor. SwiftData sortuje po wewnętrznym kluczu głównym, kompletnie ignorując Twoją implementację.
  • Brak wartości domyślnych przy nowych właściwościach — nowa właściwość bez wartości domyślnej powoduje błąd migracji. Zawsze dodawaj domyślną wartość.
  • Oczekiwanie natychmiastowej dostępności danych po save() — dane powiązane relacjami mogą nie być od razu widoczne w zapytaniach. Poczekaj na następny cykl run loop.
  • Enumy bez Codable i RawValue — SwiftData wymaga, by enumy używane jako właściwości modelu miały RawValue i konformowały do Codable. Bez tego dostaniesz tajemniczy crash w runtime.

FAQ — Najczęściej zadawane pytania

Czy SwiftData całkowicie zastąpi Core Data?

Nie w najbliższym czasie. Apple aktywnie rozwija oba frameworki. SwiftData jest zbudowany na Core Data i oba mogą współistnieć w tym samym projekcie (korzystając z tego samego pliku SQLite). Dla nowych projektów w SwiftUI — SwiftData to zdecydowanie lepsza opcja. Ale dla istniejących projektów z głęboką integracją UIKit i Core Data migracja może nie być opłacalna.

Czy mogę używać SwiftData bez SwiftUI?

Tak, jak najbardziej. Chociaż integracja ze SwiftUI (zwłaszcza @Query) jest główną zaletą frameworka, możesz spokojnie używać ModelContainer, ModelContext i FetchDescriptor w dowolnym kodzie Swift — również z UIKit. Jedyne ograniczenie: makro @Query działa wyłącznie w widokach SwiftUI.

Jak synchronizować dane SwiftData przez iCloud?

SwiftData obsługuje synchronizację iCloud przez CloudKit. Wystarczy skonfigurować cloudKitDatabase w ModelConfiguration i włączyć uprawnienie CloudKit w projekcie. SwiftData automatycznie synchronizuje dane między urządzeniami. Jedno zastrzeżenie — synchronizacja CloudKit wymaga, aby wszystkie właściwości miały wartości domyślne lub były opcjonalne.

Czy SwiftData działa z widgetami i App Intents?

Tak. SwiftData współgra z widgetami, App Intents i rozszerzeniami aplikacji. Kluczowe jest użycie App Group Container — skonfiguruj groupContainer w ModelConfiguration, żeby główna aplikacja i widget współdzieliły ten sam plik bazy danych.

Jak testować kod używający SwiftData?

Najlepsza praktyka to kontenery in-memory w testach. Tworzysz ModelContainer z flagą isStoredInMemoryOnly: true, wstawiasz testowe dane i wykonujesz asercje. Każdy test startuje z czystym stanem, testy są szybkie i od siebie niezależne. Jeśli testujesz migracje — tam potrzebujesz kontenera z trwałym zapisem i wersjonowanymi schematami.

O Autorze Editorial Team

Our team of expert writers and editors.