Uvod u SwiftData
Ruku na srce — ako ste ikada radili s Core Data u iOS aplikacijama, znate koliko taj framework zna biti frustrirajući. Složeni boilerplate kod, NSManagedObject podklase, .xcdatamodeld datoteke, ručno upravljanje kontekstima... sve to čini razvoj sporijim nego što bi trebao biti. Apple je napokon odlučio riješiti te probleme i na WWDC 2023 predstavio SwiftData — potpuno novi framework za perzistenciju podataka, dizajniran od nule za Swift i SwiftUI.
SwiftData nije samo kozmetička nadogradnja Core Data. Radi se o temeljno drugačijem pristupu koji koristi Swift makroe, deklarativnu sintaksu i duboku integraciju sa SwiftUI-jem.
A ispod haube? SwiftData i dalje koristi Core Data kao svoj storage engine, što znači da dobivate modernu API površinu uz provjerenu pouzdanost Core Data sustava. Iskreno, to je pametna odluka — zašto bacati engine koji radi već 20 godina?
U ovom vodiču proći ćemo kroz sve ključne koncepte SwiftData frameworka — od definiranja modela s @Model makroom, preko postavljanja ModelContainer-a i ModelContext-a, do naprednijih tema poput odnosa između modela, filtriranja s #Predicate makroom i migracije sheme. Sve s praktičnim primjerima koda koje možete odmah primijeniti u vlastitim projektima.
Zašto SwiftData? Problemi starog pristupa s Core Data
Da bismo razumjeli zašto je SwiftData toliko značajan, moramo se prvo prisjetiti što je sve bilo problematično s Core Data. Framework koji postoji od 2005. nosi sa sobom teret Objective-C ere i mnoge koncepte koji se jednostavno ne uklapaju u moderni Swift razvoj.
Prekomjerni boilerplate kod
Core Data zahtijeva nevjerojatnu količinu pripremnog koda prije nego što uopće možete početi raditi s podacima. Morate stvoriti .xcdatamodeld datoteku, definirati entitete u grafičkom editoru, generirati NSManagedObject podklase, postaviti persistent container, managed object context — i tek onda možete krenuti s CRUD operacijama. Puno posla za nešto što bi trebalo biti jednostavno.
// Core Data pristup - samo inicijalizacija stacka
class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "MojModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Nije moguće učitati store: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let error = error as NSError
fatalError("Greška prilikom spremanja: \(error)")
}
}
}
}
I to je samo za postavljanje stacka! Za svaki entitet trebate još definirati model u grafičkom editoru i generirati Swift klase. SwiftData pristup je, u usporedbi s ovim, dramatično jednostavniji.
Problemi s type safety
Core Data koristi string-bazirane ključeve za pristup atributima. To znači da kompajler ne može provjeriti ispravnost vašeg koda u vrijeme prevođenja. Greške u nazivima atributa otkrivate tek u runtimeu — što je, blago rečeno, frustrirajuće. Posebno u produkcijskim aplikacijama.
Slaba integracija sa SwiftUI-jem
Iako je Apple dodao @FetchRequest property wrapper za SwiftUI, integracija nikada nije bila baš prirodna. Core Data objekti nisu bili Observable, pa je komunikacija između modela i UI-a zahtijevala dodatne slojeve apstrakcije i poprilično ručnog rada.
@Model makro — Srce SwiftData frameworka
Najvažniji element SwiftData frameworka je @Model makro. On transformira obične Swift klase u perzistentne modele podataka, automatski generirajući sav potreban kod za čitanje, pisanje i praćenje promjena. Makro @Model ujedno čini vašu klasu konformnom protokolima Observable, Hashable, Identifiable i PersistentModel.
Osnovna definicija modela
import SwiftData
@Model
class Zadatak {
var naslov: String
var opis: String
var jeDovršen: Bool
var datumStvaranja: Date
var prioritet: Int
init(naslov: String, opis: String = "", prioritet: Int = 0) {
self.naslov = naslov
self.opis = opis
self.jeDovršen = false
self.datumStvaranja = Date()
self.prioritet = prioritet
}
}
To je doslovno sve što trebate za potpuno funkcionalan perzistentni model. Nema grafičkog editora, nema generiranih klasa, nema boilerplate koda.
Makro @Model automatski obrađuje sva pohranjena svojstva i pretvara ih u getere i setere koji čitaju i pišu podatke u SwiftData storage sustav. Kad sam prvi put vidio koliko je ovo jednostavno u usporedbi s Core Data, iskreno — bio sam oduševljen.
Atributi za fino podešavanje
SwiftData pruža @Attribute makro za dodatnu konfiguraciju svojstava modela. Dva najčešće korištena atributa su .unique za označavanje jedinstvenih vrijednosti i .externalStorage za pohranu velikih podataka izvan glavne baze:
@Model
class Korisnik {
var ime: String
var prezime: String
@Attribute(.unique)
var email: String
@Attribute(.externalStorage)
var profilnaSlika: Data?
@Attribute(.spotlight)
var biografija: String?
init(ime: String, prezime: String, email: String) {
self.ime = ime
self.prezime = prezime
self.email = email
}
}
Atribut .unique osigurava da ne postoje dva korisnika s istim emailom. Ako pokušate umetnuti korisnika s postojećim emailom, SwiftData će izvršiti upsert — ažurirati postojeći zapis umjesto stvaranja duplikata. Atribut .externalStorage govori SwiftData da veliku binarnu datoteku (poput slike) pohrani izvan SQLite baze, čime se poboljšavaju performanse upita.
Podržani tipovi podataka
SwiftData podržava širok spektar tipova za svojstva modela:
- Osnovni tipovi —
String,Int,Double,Float,Bool,Date,Data,URL,UUID - Kolekcije —
Array,Dictionary,Set(pod uvjetom da elementi konformirajuCodable) - Enumeracije — enum tipovi koji konformiraju
Codable - Strukturirani tipovi — bilo koji
Codablestruct - Opcionalni tipovi — svi navedeni tipovi mogu biti opcionalni
ModelContainer i ModelContext — Infrastruktura za perzistenciju
SwiftData koristi dva ključna objekta za upravljanje podacima: ModelContainer koji stvara i upravlja bazom podataka, te ModelContext koji prati promjene objekata u memoriji. Hajde da ih pogledamo malo detaljnije.
Postavljanje ModelContainera
Najjednostavniji način za postavljanje SwiftData u vašoj aplikaciji je dodavanje .modelContainer modifikatora na glavni App struct:
import SwiftUI
import SwiftData
@main
struct MojaAplikacija: App {
var body: some Scene {
WindowGroup {
PocetniPogled()
}
.modelContainer(for: [Zadatak.self, Kategorija.self])
}
}
Ovaj jedan redak koda radi nekoliko stvari istovremeno: stvara SQLite bazu podataka (ako ne postoji), registrira vaše modele, stvara glavni ModelContext i stavlja ga u SwiftUI environment. Kad se aplikacija prvi put pokrene, SwiftData kreira bazu; pri sljedećim pokretanjima učitava postojeću. Jednostavno i elegantno.
Napredna konfiguracija s ModelConfiguration
Za složenije scenarije možete koristiti ModelConfiguration za preciznije upravljanje pohranom:
@main
struct MojaAplikacija: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([Zadatak.self, Kategorija.self])
let konfiguracija = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
cloudKitDatabase: .automatic
)
do {
return try ModelContainer(
for: schema,
configurations: [konfiguracija]
)
} catch {
fatalError("Nije moguće stvoriti ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
PocetniPogled()
}
.modelContainer(sharedModelContainer)
}
}
Parametar cloudKitDatabase: .automatic automatski omogućuje iCloud sinkronizaciju vaših podataka. A parametar isStoredInMemoryOnly? Izuzetno koristan za testiranje i SwiftUI Previewe — kada je postavljen na true, podaci se čuvaju samo u memoriji i ne zapisuju na disk.
ModelContext — Rad s podacima
ModelContext je objekt koji zapravo koristite za sve operacije s podacima — umetanje, ažuriranje, brisanje i dohvaćanje. U SwiftUI pogledima pristupate mu preko environmenta:
struct MojPogled: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
Button("Dodaj zadatak") {
let noviZadatak = Zadatak(naslov: "Novi zadatak")
modelContext.insert(noviZadatak)
// Nije potrebno ručno zvati save() - SwiftData automatski sprema!
}
}
}
Jedna od najboljih značajki SwiftData je automatsko spremanje. Ne morate ručno pozivati save() nakon svake operacije — SwiftData to radi automatski na temelju UI događaja i korisničkog unosa.
Naravno, ako želite, automatsko spremanje možete isključiti postavljanjem autosaveEnabled na false u konfiguraciji containera. Ali za većinu aplikacija, zadano ponašanje je sasvim ok.
@Query makro — Dohvaćanje podataka u SwiftUI-ju
Makro @Query je SwiftData ekvivalent Core Data @FetchRequest-a, ali znatno moćniji i jednostavniji za korištenje. On automatski dohvaća podatke iz baze, prati promjene i osvježava pogled kad god se podaci promijene.
Osnovno korištenje
struct ListaZadataka: View {
// Dohvati sve zadatke, sortirane po datumu stvaranja
@Query(sort: \Zadatak.datumStvaranja, order: .reverse)
private var zadaci: [Zadatak]
@Environment(\.modelContext) private var modelContext
var body: some View {
NavigationStack {
List {
ForEach(zadaci) { zadatak in
HStack {
Image(systemName: zadatak.jeDovršen
? "checkmark.circle.fill"
: "circle")
.foregroundStyle(zadatak.jeDovršen ? .green : .gray)
VStack(alignment: .leading) {
Text(zadatak.naslov)
.strikethrough(zadatak.jeDovršen)
Text(zadatak.datumStvaranja, style: .date)
.font(.caption)
.foregroundStyle(.secondary)
}
}
.onTapGesture {
zadatak.jeDovršen.toggle()
}
}
.onDelete(perform: obrisiZadatke)
}
.navigationTitle("Moji zadaci")
}
}
private func obrisiZadatke(at offsets: IndexSet) {
for index in offsets {
modelContext.delete(zadaci[index])
}
}
}
Ono što je posebno elegantno kod @Query makroa jest da automatski prati bazu podataka za promjene. Kad dodate, izbrišete ili izmijenite zadatak bilo gdje u aplikaciji, svi pogledi koji koriste @Query za taj model automatski se ažuriraju. Nema potrebe za ručnim osvježavanjem ili obavještavanjem — jednostavno radi.
Filtriranje s #Predicate
Za složenije upite koristimo #Predicate makro koji pruža type-safe filtriranje podataka. Za razliku od Core Data NSPredicate-a koji koristi stringove, #Predicate se provjerava u vrijeme kompilacije:
struct NedovršeniZadaciPogled: View {
// Dohvati samo nedovršene zadatke visokog prioriteta
@Query(
filter: #Predicate<Zadatak> { zadatak in
zadatak.jeDovršen == false && zadatak.prioritet >= 3
},
sort: \Zadatak.prioritet,
order: .reverse
)
private var hitniZadaci: [Zadatak]
var body: some View {
List(hitniZadaci) { zadatak in
VStack(alignment: .leading) {
Text(zadatak.naslov)
.font(.headline)
Text("Prioritet: \(zadatak.prioritet)")
.font(.caption)
.foregroundStyle(.red)
}
}
}
}
Dinamičko filtriranje
Evo jednog uzorka koji ćete koristiti iznimno često — dinamičko filtriranje gdje korisnik može mijenjati kriterije pretrage. Trik je u korištenju init parametra u podpogledu:
struct FiltriranaListaZadataka: View {
@Query private var zadaci: [Zadatak]
init(pojamPretrage: String, samoNedovršeni: Bool) {
let predikat = #Predicate<Zadatak> { zadatak in
(pojamPretrage.isEmpty ||
zadatak.naslov.localizedStandardContains(pojamPretrage)) &&
(!samoNedovršeni || zadatak.jeDovršen == false)
}
_zadaci = Query(
filter: predikat,
sort: \Zadatak.datumStvaranja,
order: .reverse
)
}
var body: some View {
List(zadaci) { zadatak in
Text(zadatak.naslov)
}
}
}
// Roditeljski pogled
struct GlavniPogled: View {
@State private var pojamPretrage = ""
@State private var samoNedovršeni = false
var body: some View {
VStack {
TextField("Pretraži...", text: $pojamPretrage)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Toggle("Samo nedovršeni", isOn: $samoNedovršeni)
.padding(.horizontal)
FiltriranaListaZadataka(
pojamPretrage: pojamPretrage,
samoNedovršeni: samoNedovršeni
)
}
}
}
Roditeljski pogled drži stanje filtera, a podpogled prima parametre i na temelju njih inicijalizira @Query s odgovarajućim predikatom. Kad god se parametri promijene, SwiftUI ponovno stvara podpogled s novim upitom. Čisto i pregledno.
Odnosi između modela — @Relationship
Stvarne aplikacije rijetko imaju samo jedan model podataka. (Kad bi bilo tako jednostavno!) SwiftData podržava odnose između modela — jedan-prema-jedan, jedan-prema-mnogo i mnogo-prema-mnogo — s intuitivnom sintaksom koja koristi standardne Swift tipove.
Definiranje odnosa
@Model
class Kategorija {
var naziv: String
var boja: String
var ikona: String
@Relationship(deleteRule: .cascade, inverse: \Zadatak.kategorija)
var zadaci: [Zadatak] = []
init(naziv: String, boja: String = "blue", ikona: String = "folder") {
self.naziv = naziv
self.boja = boja
self.ikona = ikona
}
}
@Model
class Zadatak {
var naslov: String
var opis: String
var jeDovršen: Bool
var datumStvaranja: Date
var prioritet: Int
var kategorija: Kategorija?
@Relationship(deleteRule: .nullify)
var oznake: [Oznaka] = []
init(naslov: String, opis: String = "", prioritet: Int = 0) {
self.naslov = naslov
self.opis = opis
self.jeDovršen = false
self.datumStvaranja = Date()
self.prioritet = prioritet
}
}
@Model
class Oznaka {
var naziv: String
@Attribute(.unique)
var slug: String
var zadaci: [Zadatak] = []
init(naziv: String, slug: String) {
self.naziv = naziv
self.slug = slug
}
}
U ovom primjeru imamo tri modela s različitim vrstama odnosa. Kategorija ima odnos jedan-prema-mnogo sa Zadatak — jedna kategorija može sadržavati mnogo zadataka. Zadatak i Oznaka imaju odnos mnogo-prema-mnogo — svaki zadatak može imati više oznaka, i svaka oznaka može pripadati više zadataka.
Pravila brisanja
Atribut deleteRule definira što se događa s povezanim objektima kad obrišete roditeljski objekt. Ovo je bitno razumjeti jer krivi odabir može dovesti do gubitka podataka:
- .nullify (zadano) — postavlja referencu na
nilu povezanim objektima - .cascade — briše sve povezane objekte (koristite kad djeca ne mogu postojati bez roditelja)
- .deny — sprečava brisanje ako postoje povezani objekti
- .noAction — ne radi ništa s povezanim objektima
U našem primjeru, brisanje kategorije s .cascade pravilom automatski briše sve zadatke u toj kategoriji. S druge strane, brisanje zadatka s .nullify pravilom samo uklanja vezu s oznakama, ali ne briše same oznake.
Rad s odnosima u praksi
// Dodavanje zadatka u kategoriju
func dodajZadatak(u kategoriju: Kategorija, naslov: String) {
let noviZadatak = Zadatak(naslov: naslov)
noviZadatak.kategorija = kategoriju
modelContext.insert(noviZadatak)
// SwiftData automatski ažurira kategorija.zadaci
}
// Dodavanje oznake zadatku
func dodajOznaku(zadatku zadatak: Zadatak, naziv: String) {
let oznaka = Oznaka(naziv: naziv, slug: naziv.lowercased())
zadatak.oznake.append(oznaka)
// Nije potrebno eksplicitno spremati - autosave!
}
// Upit s odnosom - zadaci u određenoj kategoriji
struct ZadaciKategorijePogled: View {
var kategorija: Kategorija
var body: some View {
List(kategorija.zadaci) { zadatak in
Text(zadatak.naslov)
}
.navigationTitle(kategorija.naziv)
}
}
SwiftData koristi lijeno učitavanje (lazy loading) za odnose. To znači da se povezani objekti učitavaju iz baze tek kad im stvarno pristupite — čime se štedi memorija i poboljšavaju performanse. Praktično, to je nešto o čemu ne morate razmišljati u svakodnevnom radu, ali dobro je znati da se to događa u pozadini.
Nove mogućnosti u iOS 18 — #Index, #Unique i History API
S iOS-om 18, SwiftData je dobio značajna poboljšanja koja ga čine još moćnijim. Evo najvažnijih novosti.
#Index makro za brže upite
Makro #Index omogućuje stvaranje indeksa na svojstvima modela, čime se dramatično ubrzavaju upiti koji filtriraju ili sortiraju po tim svojstvima:
@Model
class Zadatak {
#Index<Zadatak>([\. datumStvaranja], [\.prioritet], [\.jeDovršen, \.datumStvaranja])
var naslov: String
var opis: String
var jeDovršen: Bool
var datumStvaranja: Date
var prioritet: Int
init(naslov: String, opis: String = "", prioritet: Int = 0) {
self.naslov = naslov
self.opis = opis
self.jeDovršen = false
self.datumStvaranja = Date()
self.prioritet = prioritet
}
}
Složeni indeks [\.jeDovršen, \.datumStvaranja] optimizira upite koji istovremeno filtriraju po statusu dovršenosti i sortiraju po datumu — što je upravo ono što biste najčešće radili u todo aplikaciji. Ako imate veću bazu podataka, razlika u brzini može biti osjetna.
#Unique makro za složena ograničenja jedinstvenosti
Dok @Attribute(.unique) radi za jedno svojstvo, #Unique makro omogućuje definiranje jedinstvenosti na temelju kombinacije više svojstava:
@Model
class PrijavaNaSeminar {
#Unique<PrijavaNaSeminar>([\.korisnikId, \.seminarId])
var korisnikId: String
var seminarId: String
var datumPrijave: Date
init(korisnikId: String, seminarId: String) {
self.korisnikId = korisnikId
self.seminarId = seminarId
self.datumPrijave = Date()
}
}
Ovaj primjer osigurava da se isti korisnik ne može prijaviti na isti seminar dvaput. Pokušaj umetanja duplikata rezultira upsertom umjesto greške — što je zapravo prilično elegantno rješenje.
History API za praćenje promjena
Jedna od najuzbudljivijih novih mogućnosti u iOS-u 18 je History API. Omogućuje praćenje povijesti promjena u bazi podataka, što je posebno korisno za sinkronizaciju s udaljenim serverima:
// Dohvaćanje povijesti promjena
func provjeriPromjene() async throws {
let deskriptor = HistoryDescriptor<DefaultHistoryTransaction>()
let transakcije = try modelContext.fetchHistory(deskriptor)
for transakcija in transakcije {
for promjena in transakcija.changes {
switch promjena {
case .insert(let umetanje):
print("Novi objekt umetnut: \(umetanje.changedPersistentIdentifier)")
case .update(let ažuriranje):
print("Objekt ažuriran: \(ažuriranje.changedPersistentIdentifier)")
case .delete(let brisanje):
print("Objekt obrisan: \(brisanje.changedPersistentIdentifier)")
}
}
}
}
History API otvara vrata za napredne scenarije poput inkrementalne sinkronizacije, undo/redo sustava na razini baze podataka i reagiranja na promjene iz widgeta ili ekstenzija aplikacije.
FetchDescriptor — Upiti izvan SwiftUI pogleda
Dok je @Query makro savršen za SwiftUI poglede, ponekad trebate dohvatiti podatke izvan konteksta pogleda — u view modelima, servisima ili pozadinskim zadacima. Za to služi FetchDescriptor:
// Dohvaćanje podataka s FetchDescriptor-om
func dohvatiHitneZadatke() throws -> [Zadatak] {
let predikat = #Predicate<Zadatak> { zadatak in
zadatak.jeDovršen == false && zadatak.prioritet >= 4
}
var deskriptor = FetchDescriptor<Zadatak>(
predicate: predikat,
sortBy: [SortDescriptor(\.prioritet, order: .reverse)]
)
deskriptor.fetchLimit = 10
return try modelContext.fetch(deskriptor)
}
// Brojanje objekata bez dohvaćanja svih podataka
func brojNedovršenihZadataka() throws -> Int {
let predikat = #Predicate<Zadatak> { zadatak in
zadatak.jeDovršen == false
}
let deskriptor = FetchDescriptor<Zadatak>(predicate: predikat)
return try modelContext.fetchCount(deskriptor)
}
// Brisanje više objekata odjednom
func obrisiDovršeneZadatke() throws {
let predikat = #Predicate<Zadatak> { zadatak in
zadatak.jeDovršen == true
}
try modelContext.delete(model: Zadatak.self, where: predikat)
}
Metoda fetchCount je posebno korisna jer vraća samo broj rezultata bez učitavanja samih objekata u memoriju. Za velike baze podataka, razlika u performansama može biti značajna u usporedbi s dohvaćanjem svih objekata pa brojanjem elemenata niza.
Migracija sheme — Upravljanje promjenama modela
Jedna od najvećih briga kod bilo kojeg sustava za perzistenciju jest — što se događa kad promijenite strukturu modela? Korisnici imaju postojeće baze podataka s podacima, pa ne možete jednostavno obrisati sve i krenuti ispočetka.
Automatska lagana migracija
Dobra vijest: SwiftData automatski rukuje jednostavnim promjenama poput dodavanja novih svojstava s zadanim vrijednostima ili brisanja svojstava. Za te slučajeve ne morate pisati nikakav dodatni kod — SwiftData to rješava sam.
Ručna migracija s VersionedSchema
Za složenije promjene, poput preimenovanja svojstava ili transformacije podataka, trebate definirati verzije sheme:
// Verzija 1 - originalna shema
enum SemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[ZadatakV1.self]
}
@Model
class ZadatakV1 {
var naslov: String
var dovršen: Bool
init(naslov: String, dovršen: Bool = false) {
self.naslov = naslov
self.dovršen = dovršen
}
}
}
// Verzija 2 - dodano polje prioritet i preimenovano dovršen u jeDovršen
enum SemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[ZadatakV2.self]
}
@Model
class ZadatakV2 {
var naslov: String
var jeDovršen: Bool
var prioritet: Int
init(naslov: String, jeDovršen: Bool = false, prioritet: Int = 0) {
self.naslov = naslov
self.jeDovršen = jeDovršen
self.prioritet = prioritet
}
}
}
// Plan migracije
enum PlanMigracije: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SemaV1.self, SemaV2.self]
}
static var stages: [MigrationStage] {
[migracijaV1naV2]
}
static let migracijaV1naV2 = MigrationStage.lightweight(
fromVersion: SemaV1.self,
toVersion: SemaV2.self
)
}
Za primjenu plana migracije, proslijedite ga prilikom stvaranja ModelContainer-a:
let container = try ModelContainer(
for: SemaV2.ZadatakV2.self,
migrationPlan: PlanMigracije.self
)
Važno je napomenuti da SwiftData trenutno podržava samo lagane (lightweight) migracije. Za složene transformacije podataka koje zahtijevaju prilagođenu logiku, i dalje trebate Core Data heavy-weight migracije ili ručno rješenje. To je, iskreno, jedan od većih nedostataka SwiftData u ovom trenutku.
Rad sa SwiftData u pozadini
U stvarnim aplikacijama često trebate raditi s podacima u pozadinskim nitima — na primjer, prilikom sinkronizacije s serverom ili obrade velikih skupova podataka. SwiftData ModelContext mora ostati na niti koja ga je stvorila, pa za pozadinski rad trebate stvoriti novi kontekst:
// Pozadinska obrada podataka
func sinkronizirajPodatke(container: ModelContainer) async throws {
// Dohvati podatke s servera
let podaciServera = try await dohvatiSaPoslužitelja()
// Stvori novi ModelContext za pozadinsku nit
let pozadinskiKontekst = ModelContext(container)
pozadinskiKontekst.autosaveEnabled = false
for podaci in podaciServera {
let noviZadatak = Zadatak(
naslov: podaci.naslov,
opis: podaci.opis,
prioritet: podaci.prioritet
)
pozadinskiKontekst.insert(noviZadatak)
}
// Ručno spremi sve promjene odjednom
try pozadinskiKontekst.save()
}
Ključno pravilo koje si morate zapamtiti: ModelContainer možete slobodno proslijeđivati između niti, ali ModelContext morate koristiti samo na niti koja ga je stvorila. Ako prekršite ovo pravilo, možete očekivati nepredvidive greške ili rušenja aplikacije. Vjerujte mi, debugiranje takvih problema nije zabavno.
SwiftData u Previewima i testovima
Jedna od praktičnih prednosti SwiftData je jednostavnost postavljanja za SwiftUI Previewe i unit testove. Korištenjem in-memory konfiguracije stvarate izoliran kontekst s testnim podacima:
// SwiftUI Preview s testnim podacima
#Preview {
let konfiguracija = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(
for: Zadatak.self,
configurations: konfiguracija
)
// Dodaj testne podatke
let kontekst = container.mainContext
let testniZadaci = [
Zadatak(naslov: "Napisati dokumentaciju", prioritet: 3),
Zadatak(naslov: "Popraviti bug u prijavi", prioritet: 5),
Zadatak(naslov: "Ažurirati ovisnosti", prioritet: 1)
]
testniZadaci.forEach { kontekst.insert($0) }
return ListaZadataka()
.modelContainer(container)
}
// Unit test
@Test func testDodavanjeZadatka() async throws {
let konfiguracija = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: Zadatak.self,
configurations: konfiguracija
)
let kontekst = ModelContext(container)
let zadatak = Zadatak(naslov: "Testni zadatak", prioritet: 2)
kontekst.insert(zadatak)
try kontekst.save()
let deskriptor = FetchDescriptor<Zadatak>()
let sviZadaci = try kontekst.fetch(deskriptor)
#expect(sviZadaci.count == 1)
#expect(sviZadaci.first?.naslov == "Testni zadatak")
}
Parametar isStoredInMemoryOnly: true osigurava da svaki preview i test započinje s čistom bazom podataka, bez utjecaja na stvarne podatke aplikacije. Pouzdano i ponovljivo testiranje — baš kako treba biti.
SwiftData vs Core Data — Kada koristiti koji?
S obzirom na to da oba frameworka koegzistiraju, legitimno je pitanje kada koristiti koji. Evo smjernica za 2026. godinu.
Koristite SwiftData kada:
- Počinjete novi projekt s iOS 17+ / macOS 14+ kao minimalnim zahtjevom
- Vaša aplikacija koristi SwiftUI kao primarni UI framework
- Imate relativno jednostavne modele podataka bez iznimno složenih odnosa
- Želite brži razvoj s manje boilerplate koda
- Trebate automatsku iCloud sinkronizaciju za privatne podatke
- Preferirate Swift-nativnu sintaksu i makroe
Ostanite s Core Data kada:
- Vaša aplikacija mora podržavati starije verzije iOS-a (prije iOS 17)
- Imate složene modele s dubokim hijerarhijama odnosa
- Trebate heavy-weight migracije sa složenim transformacijama podataka
- Vaša aplikacija koristi UIKit s dubokom Core Data integracijom
- Trebate dijeljene iCloud baze podataka (ne samo privatne)
- Imate specifične potrebe za Objective-C kompatibilnošću
Hibridni pristup
Ne morate birati isključivo jedan framework. Sasvim je legitimno koristiti oba istovremeno — Core Data za postojeće složene modele, a SwiftData za nove značajke. Samo pazite da obje sheme budu usklađene ako dijele istu bazu podataka.
Savjeti za debugiranje SwiftData aplikacija
Budući da SwiftData interno koristi Core Data, svi alati za debugiranje Core Data rade i sa SwiftData. Najkorisniji je launch argument za logiranje SQL upita:
// U Xcode: Product > Scheme > Edit Scheme > Run > Arguments
// Dodajte launch argument:
// -com.apple.CoreData.SQLDebug 1
// Ovo će u konzoli prikazati sve SQL upite koje SwiftData izvršava:
// CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNASLOV, t0.ZJEDOVRSEN ...
// CoreData: annotation: fetch Zadatak
Također, za pronalaženje lokacije SQLite baze podataka na simulatoru:
// -com.apple.CoreData.Logging.stderr 1
// Ili pronađite bazu ručno:
// ~/Library/Developer/CoreSimulator/Devices/[UUID]/data/Containers/Data/Application/[UUID]/Library/Application Support/
Otvaranje SQLite datoteke u alatima poput DB Browser for SQLite može vam pomoći da vizualno provjerite stanje baze i brzo identificirate probleme s podacima. Osobno koristim ovaj pristup gotovo svakodnevno kad radim sa SwiftData.
Praktični savjeti i česte zamke
Evo nekoliko savjeta koji će vam uštedjeti vrijeme i frustraciju pri radu sa SwiftData.
1. Pažljivo s #Predicate i složenim tipovima
Makro #Predicate može prevoditi samo izraze koji uspoređuju skalarne vrijednosti (brojevi, stringovi, UUID). Ne možete uspoređivati složene objekte unutar predikata — ako to pokušate, aplikacija će se srušiti u runtimeu. Umjesto toga, koristite identifikator:
// KRIVO - uzrokuje pad aplikacije
let predicate = #Predicate<Zadatak> { zadatak in
zadatak.kategorija == mojaKategorija // Runtime crash!
}
// ISPRAVNO - koristite persistentModelID
let kategorijaId = mojaKategorija.persistentModelID
let predicate = #Predicate<Zadatak> { zadatak in
zadatak.kategorija?.persistentModelID == kategorijaId
}
2. Ne zaboravite na thread safety
SwiftData modeli nisu thread-safe. Nikad ne prosljeđujte PersistentModel objekte između niti. Umjesto toga, proslijedite PersistentIdentifier i dohvatite objekt u novom kontekstu:
// KRIVO
Task.detached {
zadatak.naslov = "Ažurirano" // Opasno! Pristup iz krive niti!
}
// ISPRAVNO
let zadatakId = zadatak.persistentModelID
Task.detached {
let kontekst = ModelContext(container)
if let zadatak = kontekst.model(for: zadatakId) as? Zadatak {
zadatak.naslov = "Ažurirano"
try kontekst.save()
}
}
3. Koristite Codable strukture za složene tipove
Za kompleksne ugniježdene podatke koji ne trebaju vlastiti entitet, koristite Codable strukture:
struct Adresa: Codable {
var ulica: String
var grad: String
var postanskiBroj: String
var država: String
}
@Model
class Korisnik {
var ime: String
var adresa: Adresa? // SwiftData automatski serijalizira Codable tipove
init(ime: String, adresa: Adresa? = nil) {
self.ime = ime
self.adresa = adresa
}
}
4. Optimizirajte performanse s fetchLimit
Uvijek postavljajte fetchLimit kad ne trebate sve rezultate. Ovo je posebno važno za veće baze podataka — razlika u brzini može biti značajna:
var deskriptor = FetchDescriptor<Zadatak>(
predicate: #Predicate { $0.jeDovršen == false },
sortBy: [SortDescriptor(\.datumStvaranja, order: .reverse)]
)
deskriptor.fetchLimit = 20 // Dohvati samo prvih 20 rezultata
Zaključak
SwiftData predstavlja značajan korak naprijed u perzistenciji podataka za Apple platforme. S intuitivnom sintaksom temeljenom na makroima, dubokom integracijom sa SwiftUI-jem i automatskim upravljanjem mnoštvom složenosti koje je ranije zahtijevalo ručni rad, razvoj aplikacija postaje brži i ugodniji.
Naravno, framework i dalje sazrijeva. Nedostaje podrška za heavy-weight migracije, dijeljene iCloud baze, a performanse u nekim scenarijima još zaostaju za Core Data. No, Apple aktivno razvija SwiftData — iOS 18 je donio #Index, #Unique i History API, dok iOS 26 uvodi nasljeđivanje modela. Trend je jasan.
Za nove projekte, SwiftData je očigledan izbor. Za postojeće aplikacije s Core Data, razmotrite postupnu migraciju — koristite SwiftData za nove značajke dok postojeći kod ostaje na Core Data. U svakom slučaju, upoznavanje sa SwiftData danas znači pripremu za budućnost iOS razvoja.
Ako ste pratili naš prethodni vodič o Observation frameworku i @Observable makrou, primijetit ćete da je SwiftData dizajniran da savršeno surađuje s tim sustavom. Makro @Model automatski čini vaše modele Observable, što znači da SwiftUI pogledi reagiraju na promjene podataka bez ikakvog dodatnog koda. Zajedno, ovi frameworki tvore moderan ekosustav za izradu podatkovnih aplikacija na Apple platformama.