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.

SwiftData 2026: @Model, @Query — Kompletny Guide

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 Priya Raghavan

Priya spent six years at Instacart building the iOS shopper app, where she led the migration from UIKit to SwiftUI across 80+ screens and cut crash-free sessions from 99.2% to 99.87%. Before that, she was a contractor at a Bay Area design studio shipping App Store apps for two Fortune 500 retail clients. She focuses on practical SwiftUI architecture - what holds up when you have 12 engineers committing to the same codebase, not just toy MVVM examples. Her recent work involves The Composable Architecture, Swift concurrency migration audits, and reducing main-thread hangs on older devices like the iPhone XR that enterprise fleets still ship. Priya runs a small consultancy in Oakland and occasionally speaks at try! Swift NYC. She has been writing Swift since the Objective-C bridging days of 2015.