SwiftData útmutató: Adatperzisztencia SwiftUI-val a modelltől a migrációig

Gyakorlati SwiftData útmutató magyarul: a @Model makró használatától a @Query lekérdezéseken és kapcsolatokon át az adatmigrációig. Kódpéldákkal, komplett SwiftUI alkalmazással és teljesítmény-tippekkel.

Bevezetés: Miért van szükségünk SwiftData-ra?

Ha valaha is dolgoztál Core Data-val, pontosan tudod, miről beszélek. XML-alapú modellfájlok, NSManagedObject alosztályok, az NSFetchedResultsController beállításának végtelen ceremóniája... Kicsit olyan volt az egész, mint amikor egy egyszerű ebédhez is teríteni kell az egész étkezőasztalt.

Aztán a WWDC 2023-on az Apple bemutatta a SwiftData-t. Őszintén? Azóta nem néztem vissza.

A SwiftData egy modern, Swift-natív adatperzisztencia keretrendszer, amely a Core Data évtizedes tapasztalataira épül — de teljesen új, makróalapú API-val. Nincs szükség XML modellfájlokra, a Swift kódod maga az adatmodell. Az @Model makró, a @Query property wrapper és a #Predicate makró együttesen egy olyan rendszert adnak, ami valóban öröm használni.

Ebben az útmutatóban végigmegyünk mindenen, amit a SwiftData-ról tudni érdemes: a modellek definiálásától a haladó lekérdezéseken és kapcsolatokon át egészen az adatmigrációig. Minden szekció működő kódpéldákat tartalmaz, szóval azonnal ki tudod próbálni a saját projektedben.

Na, vágjunk bele!

A @Model makró: Adatmodellek definiálása

A SwiftData szíve a @Model makró. Rárakod egy Swift osztályra, és az automatikusan perzisztens adatmodellé válik. A fordító a háttérben mindent elintéz. Nem kell NSManagedObject-ből örökölni, nem kell XML-t szerkeszteni — egyszerűen írsz egy osztályt.

import SwiftData

@Model
class Felhasznalo {
    var nev: String
    var email: String
    var regisztraciosDatum: Date
    var aktiv: Bool

    init(nev: String, email: String, regisztraciosDatum: Date = .now, aktiv: Bool = true) {
        self.nev = nev
        self.email = email
        self.regisztraciosDatum = regisztraciosDatum
        self.aktiv = aktiv
    }
}

Ennyi. Komolyan, ez egy teljes értékű adatmodell. A @Model makró automatikusan:

  • Hozzáad egy egyedi azonosítót (PersistentIdentifier)
  • Az osztályt megfigyelhetővé teszi (az Observation keretrendszer mintájára)
  • Regisztrálja a sémát a SwiftData rendszerben
  • Kezeli a perzisztenciát és a változáskövetést

Támogatott típusok

A SwiftData elég széles palettát támogat a property-kben:

  • Alaptípusok: String, Int, Double, Float, Bool, Date, UUID, URL, Data
  • Gyűjtemények: Array, Dictionary, Set (ha az elemek Codable-ök)
  • Opcionálisok: bármelyik fenti típus opcionális változata
  • Codable struktúrák: egyedi Codable típusok (JSON-ként tárolva)
  • Enum-ok: Codable és RawRepresentable enum-ok
  • Kapcsolatok: más @Model típusok

A @Attribute makró: Finomhangolás

A @Attribute makróval részletesebben is szabályozhatod az egyes property-k viselkedését. Ez az a pont, ahol a SwiftData igazán rugalmassá válik:

import SwiftData

@Model
class Cikk {
    @Attribute(.unique) var slug: String
    var cim: String

    @Attribute(.externalStorage)
    var kepAdat: Data?

    @Attribute(.spotlight)
    var tartalom: String

    @Transient var ideiglenesJegyzet: String = ""

    var letrehozva: Date

    init(slug: String, cim: String, tartalom: String, letrehozva: Date = .now) {
        self.slug = slug
        self.cim = cim
        self.tartalom = tartalom
        self.letrehozva = letrehozva
    }
}

Mit is csinálnak pontosan ezek az attribútumok?

  • .unique — egyedi megszorítás: ha már létezik ilyen értékkel rendelkező rekord, az upsert művelet frissíti ahelyett, hogy duplikálná
  • .externalStorage — nagy adatokat (képek, fájlok) külső fájlban tárolja, nem az adatbázisban közvetlenül
  • .spotlight — a mező tartalmát indexeli a Spotlight keresés számára
  • @Transient — a property nem kerül perzisztálásra, csak a memóriában létezik

Enum-ok használata modellekben

A SwiftData kiválóan kezeli az enum-okat, és ez nagyon jól jön típusbiztos állapotkezeléshez. Személyes véleményem: ez az egyik kedvenc részem a keretrendszerben, mert a Core Data-val ez mindig kicsit nyögvenyelős volt.

import SwiftData

enum FeladatPrioritas: String, Codable, CaseIterable {
    case alacsony = "alacsony"
    case kozepes = "közepes"
    case magas = "magas"
    case surgos = "sürgős"
}

enum FeladatAllapot: String, Codable {
    case tennivalo = "tennivaló"
    case folyamatban = "folyamatban"
    case kesz = "kész"
    case torolt = "törölt"
}

@Model
class Feladat {
    var cim: String
    var leiras: String
    var prioritas: FeladatPrioritas
    var allapot: FeladatAllapot
    var hatarido: Date?
    var letrehozva: Date

    init(cim: String, leiras: String = "", prioritas: FeladatPrioritas = .kozepes, hatarido: Date? = nil) {
        self.cim = cim
        self.leiras = leiras
        self.prioritas = prioritas
        self.allapot = .tennivalo
        self.hatarido = hatarido
        self.letrehozva = .now
    }
}

ModelContainer és ModelContext: Az adatréteg beállítása

A @Model makró definiálja az adatmodellt, de ahhoz, hogy ténylegesen tárold és lekérdezd az adatokat, két további komponensre van szükséged.

Gondolj rájuk így:

  • ModelContainer — Maga az adatbázis. Ő kezeli a sémát, a tárolást és a konfigurációt.
  • ModelContext — A munkaterületed. Ezen keresztül hozol létre, módosítasz, törölsz és kérdezel le adatokat.

Beállítás SwiftUI alkalmazásban

A legegyszerűbb beállítás szó szerint egyetlen módosítót igényel a fő alkalmazásnézetben:

import SwiftUI
import SwiftData

@main
struct FeladatkezeloApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Feladat.self, Projekt.self])
    }
}

Ez automatikusan létrehozza a ModelContainer-t a megadott modelltípusokhoz, és a ModelContext-et az environment-be helyezi. Onnan aztán bármelyik gyermeknézet elérheti — nagyon kényelmes.

Haladó konfiguráció

Bonyolultabb esetekhez (és hidd el, a valós projekteknél gyorsan ide jutsz) manuálisan is létrehozhatod a container-t:

import SwiftUI
import SwiftData

@main
struct FeladatkezeloApp: App {
    let container: ModelContainer

    init() {
        let schema = Schema([
            Feladat.self,
            Projekt.self,
            Cimke.self
        ])

        let config = ModelConfiguration(
            "FeladatkezeloDB",
            schema: schema,
            isStoredInMemoryOnly: false,
            allowsSave: true,
            groupContainer: .identifier("group.com.myapp.feladatkezelo"),
            cloudKitDatabase: .private("iCloud.com.myapp.feladatkezelo")
        )

        do {
            container = try ModelContainer(for: schema, configurations: [config])
        } catch {
            fatalError("Nem sikerült létrehozni a ModelContainer-t: \(error)")
        }
    }

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

Néhány hasznos konfigurációs lehetőség:

  • isStoredInMemoryOnly: true — memória-alapú tárolás, ami teszteléshez ideális
  • groupContainer — App Group támogatás widget-ekhez és kiterjesztésekhez
  • cloudKitDatabase — iCloud szinkronizáció bekapcsolása
  • allowsSave: false — csak olvasható konfiguráció

A ModelContext használata

A ModelContext-et SwiftUI nézetekből az @Environment property wrapperrel éred el:

struct FeladatListaView: View {
    @Environment(\.modelContext) private var context

    func ujFeladatLetrehozasa() {
        let feladat = Feladat(cim: "Új feladat", prioritas: .kozepes)
        context.insert(feladat)
        // A SwiftData automatikusan menti a változásokat!
    }
}

Fontos tudni: a SwiftData alapértelmezetten automatikus mentést (autosave) használ. A ModelContext magától elmenti a változásokat, amikor a rendszer alkalmasnak látja. Ha manuális mentést szeretnél, az autosave kikapcsolható:

let context = ModelContext(container)
context.autosaveEnabled = false

// ... műveletek elvégzése ...

try context.save() // Explicit mentés

CRUD műveletek: Adatok kezelése

Nézzük végig a négy alapvető adatműveletet SwiftData-val. Spoiler: jóval egyszerűbb, mint gondolnád.

Create — Új rekord létrehozása

func feladatLetrehozasa(context: ModelContext) {
    let feladat = Feladat(
        cim: "SwiftData tutorial megírása",
        leiras: "Részletes útmutató a SwiftData használatáról",
        prioritas: .magas,
        hatarido: Calendar.current.date(byAdding: .day, value: 7, to: .now)
    )
    context.insert(feladat)
    // Nem kell explicit mentés — az autosave intézi
}

Read — Adatok lekérdezése

A SwiftData két fő módot kínál az adatok lekérdezésére. Attól függően, hogy SwiftUI nézetben vagy kódból szeretnéd használni, más-más megoldás a célravezetőbb:

// 1. FetchDescriptor használata (programatikus lekérdezés)
func feladatokLekerdezese(context: ModelContext) throws -> [Feladat] {
    let descriptor = FetchDescriptor<Feladat>(
        predicate: #Predicate { $0.allapot != .torolt },
        sortBy: [SortDescriptor(\.letrehozva, order: .reverse)]
    )
    return try context.fetch(descriptor)
}

// 2. @Query használata SwiftUI nézetben (deklaratív lekérdezés)
struct FeladatListaView: View {
    @Query(
        filter: #Predicate<Feladat> { $0.allapot != .torolt },
        sort: \.letrehozva,
        order: .reverse
    )
    private var feladatok: [Feladat]

    var body: some View {
        List(feladatok) { feladat in
            FeladatSorView(feladat: feladat)
        }
    }
}

Update — Rekord módosítása

Ez az a rész, ahol a SwiftData igazán megmutatja az erejét. A frissítés meglepően egyszerű — csak módosítod az objektum tulajdonságait, és kész:

func feladatKesznekJelolese(_ feladat: Feladat) {
    feladat.allapot = .kesz
    // Nincs szükség explicit mentésre!
    // A SwiftData automatikusan nyomon követi a változásokat
    // és elmenti őket.
}

Nincs save() hívás, nincs objectWillChange.send(), nincs semmi ceremónia. Változtatod a property-t, és megvan. Ez az egyik kedvencem az egész keretrendszerben.

Delete — Rekord törlése

func feladatTorlese(_ feladat: Feladat, context: ModelContext) {
    context.delete(feladat)
}

// Több rekord törlése egyszerre
func befejezettFeladatokTorlese(context: ModelContext) throws {
    try context.delete(model: Feladat.self, where: #Predicate {
        $0.allapot == .kesz
    })
}

Lekérdezések: @Query és #Predicate

A lekérdezések a SwiftData egyik legerősebb területe. A #Predicate makró típusbiztos, Swift-natív szűrőfeltételeket tesz lehetővé, és végre elfelejthetjük az NSPredicate string-alapú szintaxisát meg az azzal járó futásidejű hibákat. (Aki debuggolt már NSPredicate formátumhibát, az tudja, miről beszélek.)

Alapvető @Query használat

struct AktivFeladatokView: View {
    // Egyszerű lekérdezés rendezéssel
    @Query(sort: \.letrehozva, order: .reverse)
    private var osszesFeladat: [Feladat]

    var body: some View {
        List(osszesFeladat) { feladat in
            Text(feladat.cim)
        }
    }
}

Szűrés #Predicate-tel

struct SurgosFeladatokView: View {
    @Query(
        filter: #Predicate<Feladat> {
            $0.prioritas == .surgos && $0.allapot != .kesz
        },
        sort: \.hatarido
    )
    private var surgosFeladatok: [Feladat]

    var body: some View {
        List(surgosFeladatok) { feladat in
            VStack(alignment: .leading) {
                Text(feladat.cim)
                    .font(.headline)
                if let hatarido = feladat.hatarido {
                    Text("Határidő: \(hatarido.formatted(date: .abbreviated, time: .omitted))")
                        .font(.caption)
                        .foregroundStyle(.red)
                }
            }
        }
        .navigationTitle("Sürgős feladatok")
    }
}

Dinamikus lekérdezések

Mi van, ha a szűrőfeltétel futásidőben változik? Semmi gond — az @Query inicializálható a nézet init-jében is:

struct SzurtFeladatokView: View {
    @Query private var feladatok: [Feladat]

    init(prioritas: FeladatPrioritas, csakAktivak: Bool = true) {
        let szuro = #Predicate<Feladat> { feladat in
            feladat.prioritas == prioritas &&
            (!csakAktivak || feladat.allapot != .kesz)
        }
        _feladatok = Query(
            filter: szuro,
            sort: \.letrehozva,
            order: .reverse
        )
    }

    var body: some View {
        List(feladatok) { feladat in
            FeladatSorView(feladat: feladat)
        }
    }
}

Összetett predikátumok

A #Predicate makró a legtöbb Swift kifejezést támogatja, beleértve a string-műveleteket is. Lássunk néhány valós példát:

// Szöveges keresés
let keresoszoveg = "swift"
let keresoSzuro = #Predicate<Feladat> { feladat in
    feladat.cim.localizedStandardContains(keresoszoveg) ||
    feladat.leiras.localizedStandardContains(keresoszoveg)
}

// Dátum alapú szűrés
let hetEleje = Calendar.current.startOfDay(for: .now)
let hetesSzuro = #Predicate<Feladat> { feladat in
    if let hatarido = feladat.hatarido {
        return hatarido >= hetEleje
    }
    return false
}

// Több feltétel kombinálása
let osszetetSzuro = #Predicate<Feladat> { feladat in
    feladat.prioritas == .magas &&
    feladat.allapot == .tennivalo &&
    feladat.cim.localizedStandardContains(keresoszoveg)
}

FetchDescriptor haladó beállítások

A FetchDescriptor további lehetőségeket is kínál — ilyen például a lapozás vagy a lekérdezés-optimalizálás:

func lapozottLekerdez(
    context: ModelContext,
    oldal: Int,
    oldalMeret: Int = 20
) throws -> [Feladat] {
    var descriptor = FetchDescriptor<Feladat>(
        predicate: #Predicate { $0.allapot != .torolt },
        sortBy: [SortDescriptor(\.letrehozva, order: .reverse)]
    )
    descriptor.fetchLimit = oldalMeret
    descriptor.fetchOffset = oldal * oldalMeret

    return try context.fetch(descriptor)
}

// Csak a rekordok számának lekérdezése
func feladatokSzama(context: ModelContext) throws -> Int {
    let descriptor = FetchDescriptor<Feladat>(
        predicate: #Predicate { $0.allapot == .tennivalo }
    )
    return try context.fetchCount(descriptor)
}

Kapcsolatok (Relationships)

A valós alkalmazásokban az adatmodellek ritkán állnak önmagukban — szinte mindig valamilyen kapcsolatban vannak egymással. A SwiftData natívan támogatja az egy-az-egyhez, egy-a-többhöz és több-a-többhöz kapcsolatokat. Ami viszont igazán jó hír: az inverz kapcsolatokat automatikusan kezeli.

Egy-a-többhöz kapcsolat

import SwiftData

@Model
class Projekt {
    var nev: String
    var leiras: String
    var szin: String
    var letrehozva: Date

    @Relationship(deleteRule: .cascade, inverse: \Feladat.projekt)
    var feladatok: [Feladat] = []

    init(nev: String, leiras: String = "", szin: String = "blue") {
        self.nev = nev
        self.leiras = leiras
        self.szin = szin
        self.letrehozva = .now
    }
}

@Model
class Feladat {
    var cim: String
    var leiras: String
    var prioritas: FeladatPrioritas
    var allapot: FeladatAllapot
    var hatarido: Date?
    var letrehozva: Date

    var projekt: Projekt?

    init(cim: String, leiras: String = "", prioritas: FeladatPrioritas = .kozepes, hatarido: Date? = nil) {
        self.cim = cim
        self.leiras = leiras
        self.prioritas = prioritas
        self.allapot = .tennivalo
        self.hatarido = hatarido
        self.letrehozva = .now
    }
}

A @Relationship makró deleteRule paramétere határozza meg, mi történjen a kapcsolódó rekordokkal, ha a szülő törlődik:

  • .cascade — a kapcsolódó rekordok is törlődnek (a projekt törlése törli a feladatait is)
  • .nullify — a kapcsolódó rekordok megmaradnak, de a hivatkozás null-ra áll (ez az alapértelmezés)
  • .deny — nem engedi törölni a szülőt, amíg van kapcsolódó rekord
  • .noAction — nem tesz semmit (de vigyázz ezzel, mert árva rekordok maradhatnak)

Több-a-többhöz kapcsolat

@Model
class Cimke {
    var nev: String
    var szin: String

    @Relationship(inverse: \Feladat.cimkek)
    var feladatok: [Feladat] = []

    init(nev: String, szin: String = "gray") {
        self.nev = nev
        self.szin = szin
    }
}

// A Feladat modellt egészítsük ki:
@Model
class Feladat {
    var cim: String
    // ... egyéb property-k ...

    var projekt: Projekt?
    var cimkek: [Cimke] = []

    // ... init ...
}

Kapcsolatok használata kódban

// Feladat hozzáadása egy projekthez
func feladatHozzaadasaProjekthez(
    feladat: Feladat,
    projekt: Projekt
) {
    feladat.projekt = projekt
    // A SwiftData automatikusan frissíti az inverz kapcsolatot:
    // projekt.feladatok most már tartalmazza a feladatot
}

// Címke hozzáadása egy feladathoz
func cimkeHozzaadasa(
    feladat: Feladat,
    cimke: Cimke
) {
    feladat.cimkek.append(cimke)
    // Az inverz kapcsolat itt is automatikusan frissül:
    // cimke.feladatok most már tartalmazza a feladatot
}

Komplett SwiftUI alkalmazás SwiftData-val

Most rakjuk össze az eddig tanultakat egy működő feladatkezelő alkalmazássá. Ez a példa bemutatja a modellek, a lekérdezések, a CRUD műveletek és a kapcsolatok valós együttműködését — így könnyebb átlátni, hogyan illeszkedik össze a teljes kép.

import SwiftUI
import SwiftData

// MARK: - Fő alkalmazás

@main
struct FeladatkezeloApp: App {
    var body: some Scene {
        WindowGroup {
            FoNezet()
        }
        .modelContainer(for: [Projekt.self, Feladat.self, Cimke.self])
    }
}

// MARK: - Fő nézet

struct FoNezet: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \.letrehozva, order: .reverse) private var projektek: [Projekt]
    @State private var ujProjektNev = ""

    var body: some View {
        NavigationStack {
            List {
                Section {
                    HStack {
                        TextField("Új projekt neve...", text: $ujProjektNev)
                        Button("Létrehozás") {
                            let projekt = Projekt(nev: ujProjektNev)
                            context.insert(projekt)
                            ujProjektNev = ""
                        }
                        .disabled(ujProjektNev.isEmpty)
                    }
                }

                Section("Projektek") {
                    ForEach(projektek) { projekt in
                        NavigationLink(value: projekt) {
                            VStack(alignment: .leading) {
                                Text(projekt.nev)
                                    .font(.headline)
                                Text("\(projekt.feladatok.count) feladat")
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                            }
                        }
                    }
                    .onDelete { indexSet in
                        for index in indexSet {
                            context.delete(projektek[index])
                        }
                    }
                }
            }
            .navigationTitle("Feladatkezelő")
            .navigationDestination(for: Projekt.self) { projekt in
                ProjektReszletekView(projekt: projekt)
            }
        }
    }
}

// MARK: - Projekt részletek

struct ProjektReszletekView: View {
    @Environment(\.modelContext) private var context
    @Bindable var projekt: Projekt
    @State private var ujFeladatCim = ""
    @State private var valasztottPrioritas: FeladatPrioritas = .kozepes

    var aktivFeladatok: [Feladat] {
        projekt.feladatok
            .filter { $0.allapot != .kesz && $0.allapot != .torolt }
            .sorted { $0.letrehozva > $1.letrehozva }
    }

    var befejezettFeladatok: [Feladat] {
        projekt.feladatok
            .filter { $0.allapot == .kesz }
            .sorted { $0.letrehozva > $1.letrehozva }
    }

    var body: some View {
        List {
            Section {
                HStack {
                    TextField("Új feladat...", text: $ujFeladatCim)
                    Picker("Prioritás", selection: $valasztottPrioritas) {
                        ForEach(FeladatPrioritas.allCases, id: \.self) { p in
                            Text(p.rawValue).tag(p)
                        }
                    }
                    .labelsHidden()
                    Button("Hozzáadás") {
                        let feladat = Feladat(
                            cim: ujFeladatCim,
                            prioritas: valasztottPrioritas
                        )
                        feladat.projekt = projekt
                        context.insert(feladat)
                        ujFeladatCim = ""
                    }
                    .disabled(ujFeladatCim.isEmpty)
                }
            }

            if !aktivFeladatok.isEmpty {
                Section("Aktív feladatok") {
                    ForEach(aktivFeladatok) { feladat in
                        FeladatSorView(feladat: feladat)
                    }
                }
            }

            if !befejezettFeladatok.isEmpty {
                Section("Befejezett") {
                    ForEach(befejezettFeladatok) { feladat in
                        FeladatSorView(feladat: feladat)
                    }
                }
            }
        }
        .navigationTitle(projekt.nev)
    }
}

// MARK: - Feladat sor nézet

struct FeladatSorView: View {
    @Bindable var feladat: Feladat

    var body: some View {
        HStack {
            Button {
                withAnimation {
                    feladat.allapot = feladat.allapot == .kesz ? .tennivalo : .kesz
                }
            } label: {
                Image(systemName: feladat.allapot == .kesz ? "checkmark.circle.fill" : "circle")
                    .foregroundStyle(feladat.allapot == .kesz ? .green : .gray)
            }
            .buttonStyle(.plain)

            VStack(alignment: .leading) {
                Text(feladat.cim)
                    .strikethrough(feladat.allapot == .kesz)
                    .foregroundStyle(feladat.allapot == .kesz ? .secondary : .primary)

                Text(feladat.prioritas.rawValue.capitalized)
                    .font(.caption)
                    .padding(.horizontal, 6)
                    .padding(.vertical, 2)
                    .background(prioritasSzin(feladat.prioritas).opacity(0.2))
                    .clipShape(Capsule())
            }

            Spacer()

            if let hatarido = feladat.hatarido {
                Text(hatarido.formatted(date: .abbreviated, time: .omitted))
                    .font(.caption2)
                    .foregroundStyle(hatarido < .now ? .red : .secondary)
            }
        }
    }

    func prioritasSzin(_ prioritas: FeladatPrioritas) -> Color {
        switch prioritas {
        case .alacsony: return .blue
        case .kozepes: return .orange
        case .magas: return .red
        case .surgos: return .purple
        }
    }
}

Adatmigráció: VersionedSchema és MigrationPlan

Ahogy az alkalmazásod fejlődik, az adatmodelled is változni fog. Új property-ket adsz hozzá, meglévőket törlsz vagy átnevezel. Ez elkerülhetetlen. A SwiftData a VersionedSchema és a SchemaMigrationPlan segítségével kezeli ezeket a változásokat.

Könnyű migráció (Lightweight Migration)

Ha csak új, opcionális property-t adsz hozzá, vagy olyat, aminek van alapértelmezett értéke, a SwiftData automatikusan kezeli a migrációt. Ilyenkor nem kell semmit sem csinálnod:

// Ez a változás automatikus migrációt kap:
@Model
class Feladat {
    var cim: String
    var leiras: String
    var prioritas: FeladatPrioritas
    var allapot: FeladatAllapot
    var hatarido: Date?
    var letrehozva: Date
    var megjegyzes: String? // ÚJ — opcionális, tehát automatikus migráció
    var projekt: Projekt?
}

Verziózott séma és egyedi migráció

Bonyolultabb változásokhoz — mint egy property átnevezése, típusváltozás, vagy adattranszformáció — már verziózott sémákat kell definiálnod. Ez első ránézésre sok kódnak tűnhet, de a logikája egyszerű:

import SwiftData

// 1. verzió
enum FeladatSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Feladat.self]
    }

    @Model
    class Feladat {
        var cim: String
        var kesz: Bool
        var letrehozva: Date

        init(cim: String, kesz: Bool = false) {
            self.cim = cim
            self.kesz = kesz
            self.letrehozva = .now
        }
    }
}

// 2. verzió — 'kesz' Bool helyett 'allapot' enum
enum FeladatSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Feladat.self]
    }

    @Model
    class Feladat {
        var cim: String
        var allapot: String
        var prioritas: String
        var letrehozva: Date

        init(cim: String, allapot: String = "tennivaló", prioritas: String = "közepes") {
            self.cim = cim
            self.allapot = allapot
            self.prioritas = prioritas
            self.letrehozva = .now
        }
    }
}

// Migrációs terv
enum FeladatMigraciosTerv: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [FeladatSchemaV1.self, FeladatSchemaV2.self]
    }

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

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: FeladatSchemaV1.self,
        toVersion: FeladatSchemaV2.self
    ) { context in
        let regi = try context.fetch(FetchDescriptor<FeladatSchemaV1.Feladat>())
        for feladat in regi {
            // Az egyedi migrációs logikát itt végezzük el
        }
        try context.save()
    }
}

A migrációs terv alkalmazása

@main
struct FeladatkezeloApp: App {
    let container: ModelContainer

    init() {
        do {
            container = try ModelContainer(
                for: FeladatSchemaV2.Feladat.self,
                migrationPlan: FeladatMigraciosTerv.self
            )
        } catch {
            fatalError("Migrációs hiba: \(error)")
        }
    }

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

Teljesítmény és bevált gyakorlatok

A SwiftData egy erőteljes keretrendszer, de mint minden adatbázis-technológiánál, itt is figyelni kell a teljesítményre. Néhány fontosabb tipp, amit érdemes észben tartani:

1. Használj #Index-et a gyakran szűrt mezőkhöz

@Model
class Feladat {
    #Index<Feladat>([\.allapot, \.prioritas])
    #Index<Feladat>([\.letrehozva])

    var cim: String
    var allapot: FeladatAllapot
    var prioritas: FeladatPrioritas
    var letrehozva: Date
    // ...
}

Az indexek drámaian felgyorsíthatják a lekérdezéseket, különösen nagy adathalmazok esetén. De azért ne indexelj mindent válogatás nélkül — minden index plusz írási költséget jelent.

2. Korlátozd a lekérdezés eredményeit

var descriptor = FetchDescriptor<Feladat>()
descriptor.fetchLimit = 50 // Maximum 50 eredmény
descriptor.propertiesToFetch = [\.cim, \.allapot] // Csak a szükséges mezők

3. Használj háttérszálat nagy műveletekhez

// A @ModelActor makró biztonságos háttérszálú adatkezelést tesz lehetővé
@ModelActor
actor HatterAdatkezelo {
    func tomegesImport(adatok: [FeladatDTO]) throws {
        for adat in adatok {
            let feladat = Feladat(
                cim: adat.cim,
                prioritas: .kozepes
            )
            modelContext.insert(feladat)
        }
        try modelContext.save()
    }
}

4. Kerüld az N+1 lekérdezési problémát

Ha egy listában megjeleníted a projekteket és azok feladatainak számát, a SwiftData alapértelmezetten lusta betöltést (lazy loading) használ a kapcsolatokhoz. Nagy adathalmazoknál érdemes előre betölteni a kapcsolatokat a relationshipKeyPathsForPrefetching segítségével:

var descriptor = FetchDescriptor<Projekt>()
descriptor.relationshipKeyPathsForPrefetching = [\.feladatok]
let projektek = try context.fetch(descriptor)

5. Gyakori buktatók

  • Ne használj @Model-t struct-oknál — a SwiftData kizárólag osztályokkal működik, mert referencia-szemantikára van szüksége a változáskövetéshez
  • Teszteléshez használj memória-alapú tárolástModelConfiguration(isStoredInMemoryOnly: true)
  • Vigyázz a szálbiztonsággal — a ModelContext nem szálbiztos, háttérműveletekhez használj külön kontextust vagy @ModelActor-t
  • Ne feledd az autosave-et — a változások automatikusan mentődnek, ami néha meglepő lehet (például ha a felhasználó „mégsem" gombot nyom, de az adat már elmentődött)

Gyakran ismételt kérdések (GYIK)

A SwiftData leváltja a Core Data-t?

A SwiftData a Core Data-ra épül, és hosszú távon valószínűleg felváltja azt az új projektekben. A Core Data azonban továbbra is támogatott és karbantartott — az Apple nem jelentette be a kivezetését. Ha van egy meglévő, jól működő Core Data alkalmazásod, nincs sürgős ok a migrációra. Új projekteknél viszont a SwiftData az ajánlott választás, különösen ha SwiftUI-val dolgozol.

Működik a SwiftData CloudKit-tel?

Igen, működik. A ModelConfiguration-ben megadhatod a cloudKitDatabase paramétert a szinkronizáció bekapcsolásához. A háttérben a SwiftData a Core Data CloudKit integrációját használja. Egy fontos kitétel: CloudKit szinkronizáció használatakor az @Attribute(.unique) nem alkalmazható — ehelyett a CloudKit saját rekordazonosítóira kell támaszkodnod.

Hogyan teszteljem a SwiftData kódot?

A legegyszerűbb módja a memória-alapú konfiguráció használata. Hozz létre egy ModelContainer-t isStoredInMemoryOnly: true beállítással, és abban végezd a teszteket. Így minden teszt tiszta állapotból indul, és nem kell adatbázisfájlok takarításával bajlódnod.

Használhatom a SwiftData-t UIKit-tel, SwiftUI nélkül?

Igen, a SwiftData UIKit alkalmazásokban is használható. Ebben az esetben a @Query property wrapper nem áll rendelkezésre (az SwiftUI-specifikus), de a ModelContainer, a ModelContext és a FetchDescriptor programatikusan gond nélkül működnek. A változásokra való reagáláshoz a Combine keretrendszert vagy a NotificationCenter-t érdemes használni.

Mi a különbség a @Query és a FetchDescriptor között?

A @Query egy SwiftUI property wrapper, ami automatikusan frissíti a nézetet, amikor az adatok változnak — deklaratív megközelítés, hasonlóan a régi @FetchRequest-hez a Core Data-ban. A FetchDescriptor viszont egy imperatív API, amivel bármikor lekérdezhetsz adatokat a ModelContext-en keresztül. Röviden: a @Query-t használd SwiftUI nézetekben, a FetchDescriptor-t pedig ViewModel-ekben, szolgáltatásokban vagy háttérszálon futó kódban.

A Szerzőről Editorial Team

Our team of expert writers and editors.