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
@Modeltí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álisgroupContainer— App Group támogatás widget-ekhez és kiterjesztésekhezcloudKitDatabase— iCloud szinkronizáció bekapcsolásaallowsSave: 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ást —
ModelConfiguration(isStoredInMemoryOnly: true) - Vigyázz a szálbiztonsággal — a
ModelContextnem 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.