Introducere: De Ce SwiftData Schimbă Regulile Jocului
Dacă ați dezvoltat vreodată o aplicație iOS care trebuia să salveze date local — fie că era vorba de o simplă listă de sarcini, un jurnal personal sau un catalog de produse — știți prea bine cât de frustrant putea fi Core Data. Fișiere .xcdatamodeld, context-uri de obiecte, tone de cod boilerplate doar pentru a salva un obiect banal. Ei bine, Apple a ascultat în cele din urmă comunitatea de dezvoltatori și a lansat SwiftData la WWDC 2023 — un framework modern de persistență care se integrează nativ cu SwiftUI.
Sincer, când am încercat SwiftData prima dată, am rămas surprins de cât de mult cod am putut șterge dintr-un proiect existent. Nu e doar un wrapper peste Core Data (deși folosește Core Data sub capotă). E o reimaginare completă a persistenței datelor, construită pe macro-uri Swift, observare automată și o sintaxă declarativă care se simte natural în contextul SwiftUI.
Să intrăm direct în subiect. În acest ghid vom acoperi totul — de la configurarea inițială și operațiile CRUD, până la relații complexe, predicate avansate, istoricul modificărilor și sincronizarea cu CloudKit. Toate exemplele sunt funcționale și le puteți copia direct în proiectele voastre.
Configurarea Inițială a SwiftData
Adăugarea SwiftData într-un Proiect Nou
Dacă creați un proiect nou în Xcode, puteți selecta SwiftData ca opțiune de stocare direct din asistentul de creare. Xcode generează automat structura necesară, inclusiv un model de exemplu și configurarea container-ului.
Pentru un proiect existent? Și mai simplu — importați framework-ul și gata:
import SwiftData
ModelContainer și ModelContext
Două concepte fundamentale pe care trebuie să le înțelegeți: ModelContainer și ModelContext. Gândiți-vă la ModelContainer ca la baza de date în sine — locul fizic unde stau datele. ModelContext e spațiul de lucru temporar unde faceți modificări înainte ca acestea să fie persistate (un fel de „ciornă" a bazei de date).
Configurarea se face la nivelul aplicației:
import SwiftUI
import SwiftData
@main
struct AplicatiaMeaApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Produs.self, Categorie.self])
}
}
Modificatorul .modelContainer(for:) creează automat un container persistent pentru tipurile de model specificate. Toate view-urile din interiorul WindowGroup vor avea acces la același container și context.
Configurare Avansată a Container-ului
Pentru scenarii mai complexe, puteți configura container-ul manual:
@main
struct AplicatiaMeaApp: App {
let container: ModelContainer
init() {
let schema = Schema([Produs.self, Categorie.self])
let config = ModelConfiguration(
"BazaDeDate",
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
do {
container = try ModelContainer(for: schema, configurations: [config])
} catch {
fatalError("Nu s-a putut crea ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Opțiunea isStoredInMemoryOnly: true e foarte utilă pentru previzualizări Xcode și teste unitare, unde nu vreți să persistați date pe disc. Merită reținut acest truc.
Definirea Modelelor cu @Model
Bazele Macro-ului @Model
Macro-ul @Model e piesa centrală a întregului SwiftData. Aplicat unei clase, transformă automat acea clasă într-un model persistent cu observare integrată. Adio fișiere de model vizual din Core Data — aici totul se face în cod Swift pur:
import SwiftData
@Model
class Produs {
var nume: String
var descriere: String
var pret: Double
var esteDisponibil: Bool
var dataAdaugarii: Date
init(nume: String, descriere: String, pret: Double, esteDisponibil: Bool = true) {
self.nume = nume
self.descriere = descriere
self.pret = pret
self.esteDisponibil = esteDisponibil
self.dataAdaugarii = Date()
}
}
Câteva lucruri importante de reținut:
- Modelele SwiftData trebuie să fie clase, nu structuri — sunt obiecte de referință gestionate de framework
- Toate proprietățile stocabile sunt persistate automat — nu trebuie să marcați fiecare proprietate individual
- Macro-ul
@Modelfolosește același sistem de observare ca@Observable, deci modificările se propagă automat către view-urile SwiftUI
Atribute Speciale cu @Attribute
Macro-ul @Attribute vă permite să personalizați comportamentul proprietăților individuale. Aici lucrurile devin cu adevărat interesante:
@Model
class Utilizator {
@Attribute(.unique) var email: String
@Attribute(.externalStorage) var fotoProfil: Data?
@Attribute(.spotlight) var numeComplet: String
@Attribute(.preserveValueOnDeletion) var codUnic: String
var dataInregistrarii: Date
init(email: String, numeComplet: String, codUnic: String) {
self.email = email
self.numeComplet = numeComplet
self.codUnic = codUnic
self.dataInregistrarii = Date()
}
}
Ce face fiecare atribut:
- .unique — garantează unicitatea valorii; la coliziune, SwiftData face automat un upsert (actualizare în loc de inserare duplicat)
- .externalStorage — stochează datele mari (imagini, fișiere) separat de baza de date principală, ceea ce îmbunătățește semnificativ performanța
- .spotlight — face proprietatea indexabilă prin Spotlight Search pe dispozitivul utilizatorului
- .preserveValueOnDeletion — păstrează valoarea ca „tombstone" în istoricul modificărilor, chiar și după ștergere (nou în iOS 18)
Proprietăți Tranzitorii
Aveți proprietăți care nu trebuie salvate în baza de date? Marcați-le cu @Transient:
@Model
class Articol {
var titlu: String
var continut: String
@Transient var esteEditat: Bool = false
init(titlu: String, continut: String) {
self.titlu = titlu
self.continut = continut
}
}
Constrângeri de Unicitate Compuse cu #Unique (iOS 18+)
Începând cu iOS 18, puteți defini constrângeri de unicitate pe combinații de proprietăți. Asta e foarte util în practică:
@Model
class Recenzie {
#Unique([\.utilizatorId, \.produsId])
var utilizatorId: String
var produsId: String
var rating: Int
var comentariu: String
init(utilizatorId: String, produsId: String, rating: Int, comentariu: String) {
self.utilizatorId = utilizatorId
self.produsId = produsId
self.rating = rating
self.comentariu = comentariu
}
}
Practic, un utilizator poate lăsa o singură recenzie per produs — dacă încearcă din nou, înregistrarea existentă e actualizată automat. Simplu și elegant.
Operații CRUD: Creare, Citire, Actualizare, Ștergere
Crearea Obiectelor (Create)
Pentru a crea și salva un obiect nou, folosiți modelContext.insert():
struct AdaugaProdusView: View {
@Environment(\.modelContext) private var modelContext
@State private var nume = ""
@State private var pret = ""
@Environment(\.dismiss) private var dismiss
var body: some View {
Form {
TextField("Numele produsului", text: $nume)
TextField("Prețul", text: $pret)
.keyboardType(.decimalPad)
Button("Salvează") {
let pretDouble = Double(pret) ?? 0
let produsNou = Produs(
nume: nume,
descriere: "",
pret: pretDouble
)
modelContext.insert(produsNou)
dismiss()
}
}
.navigationTitle("Produs Nou")
}
}
Un aspect care mi se pare genial la SwiftData: salvarea automată. Spre deosebire de Core Data, unde trebuia să apelați explicit context.save(), SwiftData salvează automat modificările la schimbările de stare ale interfeței. Totuși, dacă aveți nevoie, puteți forța salvarea manuală:
try modelContext.save()
Citirea Datelor cu @Query (Read)
Macro-ul @Query e modul principal de a interoga date din SwiftData în view-uri SwiftUI. Se actualizează automat când datele se modifică — fără NotificationCenter, fără delegates, fără nimic altceva:
struct ListaProduseView: View {
@Query(sort: \Produs.nume) private var produse: [Produs]
var body: some View {
List(produse) { produs in
HStack {
VStack(alignment: .leading) {
Text(produs.nume)
.font(.headline)
Text(produs.descriere)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text("\(produs.pret, specifier: "%.2f") lei")
.font(.subheadline)
.bold()
}
}
.navigationTitle("Produse")
}
}
Puteți personaliza interogarea cu sortare, filtrare și multe altele:
// Sortare descendentă după preț
@Query(sort: \Produs.pret, order: .reverse) private var produse: [Produs]
// Filtrare cu predicat
@Query(filter: #Predicate { $0.esteDisponibil == true },
sort: \Produs.dataAdaugarii)
private var produseDisponibile: [Produs]
// Limitarea rezultatelor
@Query(sort: \Produs.dataAdaugarii, order: .reverse)
private var ultimeleProduse: [Produs]
Actualizarea Obiectelor (Update)
Aici SwiftData strălucește cu adevărat. Actualizarea e remarcabil de simplă — modificați direct proprietățile obiectului și atât:
struct EditareProdusView: View {
@Bindable var produs: Produs
var body: some View {
Form {
TextField("Nume", text: $produs.nume)
TextField("Descriere", text: $produs.descriere)
Toggle("Disponibil", isOn: $produs.esteDisponibil)
}
.navigationTitle("Editare Produs")
}
}
Observați @Bindable — permite crearea de binding-uri direct pe proprietățile modelului. Modificările sunt detectate automat și persistate, fără niciun apel suplimentar. Dacă veniți din Core Data, asta va fi probabil momentul în care veți spune „de ce n-a fost dintotdeauna așa?".
Ștergerea Obiectelor (Delete)
Ștergerea se face prin modelContext.delete():
struct ListaProduseView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Produs.nume) private var produse: [Produs]
var body: some View {
List {
ForEach(produse) { produs in
Text(produs.nume)
}
.onDelete(perform: stergeProduse)
}
}
private func stergeProduse(la offsets: IndexSet) {
for index in offsets {
modelContext.delete(produse[index])
}
}
}
Interogări cu FetchDescriptor în Afara View-urilor
Când aveți nevoie să interogați date în afara unei vederi SwiftUI — de exemplu, într-un ViewModel sau într-un serviciu — folosiți FetchDescriptor:
func cautaProduse(cuNume nume: String) throws -> [Produs] {
let predicat = #Predicate { produs in
produs.nume.localizedStandardContains(nume)
}
let descriptor = FetchDescriptor(
predicate: predicat,
sortBy: [SortDescriptor(\Produs.nume)]
)
return try modelContext.fetch(descriptor)
}
// Numărarea rezultatelor fără a le încărca în memorie
func numaraProduse() throws -> Int {
let descriptor = FetchDescriptor()
return try modelContext.fetchCount(descriptor)
}
Relații între Modele
Relații One-to-Many (Unu-la-Mai-Mulți)
Relațiile sunt probabil cel mai important concept de stăpânit în SwiftData. Să luăm un exemplu concret: o categorie poate conține mai multe produse, dar un produs aparține unei singure categorii.
@Model
class Categorie {
var nume: String
var descriere: String
@Relationship(deleteRule: .cascade, inverse: \Produs.categorie)
var produse: [Produs] = []
init(nume: String, descriere: String) {
self.nume = nume
self.descriere = descriere
}
}
@Model
class Produs {
var nume: String
var pret: Double
var categorie: Categorie?
init(nume: String, pret: Double, categorie: Categorie? = nil) {
self.nume = nume
self.pret = pret
self.categorie = categorie
}
}
Regulile de ștergere sunt esențiale și merită atenție:
- deleteRule: .cascade — când o categorie e ștearsă, toate produsele ei se șterg automat
- deleteRule: .nullify (implicit) — la ștergerea categoriei, proprietatea
categoriea produselor devinenil - deleteRule: .deny — împiedică ștergerea categoriei dacă are produse asociate
- inverse: — definește explicit relația inversă, esențială pentru consistența datelor și obligatorie pentru sincronizarea CloudKit
Relații Many-to-Many (Mai-Mulți-la-Mai-Mulți)
Relațiile many-to-many apar când ambele părți pot avea referințe multiple. Un produs poate avea mai multe etichete, iar o etichetă poate fi aplicată mai multor produse:
@Model
class Produs {
var nume: String
var pret: Double
@Relationship(inverse: \Eticheta.produse)
var etichete: [Eticheta] = []
init(nume: String, pret: Double) {
self.nume = nume
self.pret = pret
}
}
@Model
class Eticheta {
var nume: String
var produse: [Produs] = []
init(nume: String) {
self.nume = nume
}
}
Un lucru care m-a prins pe picior greșit la început: SwiftData nu deduce automat relațiile many-to-many. Trebuie să folosiți explicit @Relationship cu parametrul inverse pentru ca framework-ul să creeze corect tabela de joncțiune în baza de date SQLite subiacentă.
Inserarea Obiectelor Relaționate
SwiftData inserează automat obiectele relaționate. Dacă creați o categorie cu produse, nu trebuie să inserați fiecare produs separat:
let categorieNoua = Categorie(nume: "Electronice", descriere: "Gadgeturi și dispozitive")
let produs1 = Produs(nume: "iPhone 16 Pro", pret: 5999, categorie: categorieNoua)
let produs2 = Produs(nume: "MacBook Air M4", pret: 7499, categorie: categorieNoua)
categorieNoua.produse = [produs1, produs2]
// Inserați DOAR categoria — produsele se inserează automat!
modelContext.insert(categorieNoua)
Atenție mare aici: Dacă încercați să inserați și categoria și produsele separat, veți obține eroarea „Duplicate registration attempt". E o greșeală frecventă — lăsați SwiftData să gestioneze inserarea prin relații.
Predicate și Filtrare Avansată
Bazele Macro-ului #Predicate
Macro-ul #Predicate înlocuiește vechiul NSPredicate din Core Data. Diferența majoră? Predicatele SwiftData sunt verificate la compilare. Nu mai puteți scrie predicate invalide care se stricau doar la runtime — dacă greșiți, Xcode vă spune imediat:
// Filtrare simplă
let produseScumpe = #Predicate { produs in
produs.pret > 1000
}
// Filtrare cu text
let cautare = #Predicate { produs in
produs.nume.localizedStandardContains("iPhone")
}
// Filtrare combinată
let filtruComplex = #Predicate { produs in
produs.pret > 500 && produs.esteDisponibil == true
}
Predicate pe Relații
Puteți filtra obiecte pe baza relațiilor lor. De exemplu, pentru a găsi produsele dintr-o anumită categorie:
let idCategorie = categorieCautata.persistentModelID
let predicatRelatie = #Predicate { produs in
produs.categorie?.persistentModelID == idCategorie
}
// Filtrare pe proprietatea relației
let predicatNume = #Predicate { produs in
produs.categorie?.nume == "Electronice"
}
Interogări Dinamice
Un pattern pe care îl veți folosi des: modificarea predicatului în funcție de inputul utilizatorului. Iată cum implementați o căutare dinamică:
struct CautareProduseView: View {
@State private var textCautare = ""
@State private var pretMinim: Double = 0
var body: some View {
NavigationStack {
RezultateCautareView(textCautare: textCautare, pretMinim: pretMinim)
.searchable(text: $textCautare, prompt: "Caută produse...")
}
}
}
struct RezultateCautareView: View {
@Query private var produse: [Produs]
init(textCautare: String, pretMinim: Double) {
let cautare = textCautare
let pret = pretMinim
_produse = Query(
filter: #Predicate { produs in
(cautare.isEmpty || produs.nume.localizedStandardContains(cautare))
&& produs.pret >= pret
},
sort: [SortDescriptor(\Produs.nume)]
)
}
var body: some View {
List(produse) { produs in
Text("\(produs.nume) - \(produs.pret, specifier: "%.2f") lei")
}
}
}
Notă importantă: Variabilele folosite în interiorul #Predicate trebuie să fie variabile locale. Dacă încercați să accesați proprietăți printr-un keypath cu mai multe componente pe obiecte @Observable, veți obține un crash la runtime. Am învățat asta pe propria piele.
Indexare pentru Performanță cu #Index (iOS 18+)
Începând cu iOS 18, puteți adăuga indexuri pe proprietățile modelelor pentru a accelera interogările:
@Model
class Produs {
#Index([\.nume])
#Index([\.pret])
#Index([\.categorie, \.pret]) // Index compus
var nume: String
var pret: Double
var categorie: Categorie?
var esteDisponibil: Bool
init(nume: String, pret: Double) {
self.nume = nume
self.pret = pret
self.esteDisponibil = true
}
}
Recomandarea mea: adăugați indexuri pe proprietățile pe care le folosiți frecvent în predicate și sortări. Diferența de performanță pe seturi mari de date e substanțială.
Sincronizarea cu iCloud prin CloudKit
Configurarea CloudKit
Unul dintre avantajele mari ale SwiftData e integrarea nativă cu CloudKit. Și configurarea e surprinzător de simplă:
- Deschideți Signing & Capabilities în setările target-ului
- Adăugați capabilitatea iCloud și selectați CloudKit
- Creați un container CloudKit (de exemplu,
iCloud.com.exemplu.aplicatiamea) - Adăugați Background Modes și bifați Remote Notifications
Din punct de vedere al codului, nu trebuie să modificați nimic. Serios — SwiftData se ocupă automat de tot.
Cerințe pentru Modele Compatibile cu CloudKit
Aici trebuie să fiți atenți, pentru că CloudKit impune câteva restricții stricte. Dacă nu le respectați, sincronizarea va eșua silențios — fără nicio eroare vizibilă, ceea ce face debugging-ul un coșmar:
- Nu puteți folosi
@Attribute(.unique)pe proprietăți sincronizate — CloudKit nu suportă constrângeri de unicitate - Toate proprietățile trebuie să aibă valori implicite sau să fie opționale
- Toate relațiile trebuie să fie opționale și să aibă o relație inversă definită explicit
Iată un model corect configurat pentru CloudKit:
@Model
class NotaPersonala {
var titlu: String = ""
var continut: String = ""
var dataCrearii: Date = Date()
var esteImportanta: Bool = false
@Relationship(deleteRule: .cascade, inverse: \SubNota.notaParinte)
var subNote: [SubNota]? = []
init(titlu: String, continut: String) {
self.titlu = titlu
self.continut = continut
}
}
@Model
class SubNota {
var text: String = ""
var notaParinte: NotaPersonala?
init(text: String, notaParinte: NotaPersonala? = nil) {
self.text = text
self.notaParinte = notaParinte
}
}
Sfaturi pentru Testarea Sincronizării
Simulatorul iOS e notoriu pentru problemele cu sincronizarea iCloud. Câteva sfaturi practice:
- Folosiți dispozitive fizice — minimum două pentru a verifica sincronizarea bidirecțională
- Verificați datele în CloudKit Console pentru a confirma că obiectele ajung pe server
- Aveți răbdare — sincronizarea poate dura de la câteva secunde la câteva minute, deci nu intrați în panică dacă nu vedeți rezultate imediat
Istoricul Modificărilor cu SwiftData History (iOS 18+)
Ce Este SwiftData History
Introdus la WWDC 2024, SwiftData History vă permite să urmăriți toate modificările din baza de date de-a lungul timpului. E esențial pentru sincronizarea cu un server propriu, procesarea modificărilor din extensii de aplicație sau implementarea unui sistem de audit.
Tranzacții și Modificări
Istoricul e compus din tranzacții și modificări. O tranzacție grupează toate schimbările de la o salvare a contextului, iar fiecare modificare reprezintă un model inserat, actualizat sau șters:
func proceseazaIstoric() throws {
let descriptor = HistoryDescriptor()
let tranzactii = try modelContext.fetchHistory(descriptor)
for tranzactie in tranzactii {
for modificare in tranzactie.changes {
switch modificare {
case let inserare as DefaultHistoryInsert:
print("Produs inserat: \(inserare.modelID)")
case let actualizare as DefaultHistoryUpdate:
print("Produs actualizat: \(actualizare.modelID)")
case let stergere as DefaultHistoryDelete:
print("Produs șters: \(stergere.modelID)")
default:
break
}
}
}
}
Token-uri pentru Urmărirea Progresului
Sistemul folosește token-uri care acționează ca marcaje în fluxul de tranzacții. Salvați ultimul token procesat pentru a ști de unde să reluați data viitoare:
// Salvarea token-ului
var ultimulToken: DefaultHistoryToken?
func sincronizeazaModificari() throws {
let descriptor = HistoryDescriptor(after: ultimulToken)
let tranzactiiNoi = try modelContext.fetchHistory(descriptor)
for tranzactie in tranzactiiNoi {
// Procesează modificările
proceseazaTranzactia(tranzactie)
}
// Actualizează token-ul
ultimulToken = tranzactiiNoi.last?.token
}
Migrarea Schemei
Migrări Lightweight (Automate)
SwiftData suportă migrări automate pentru schimbări simple — adăugarea de proprietăți noi cu valori implicite, ștergerea proprietăților sau redenumirea lor. Se întâmplă automat, fără intervenția voastră. Un lucru în minus de care trebuie să vă faceți griji.
Migrări Personalizate cu VersionedSchema
Pentru schimbări mai complexe, aveți nevoie de VersionedSchema și SchemaMigrationPlan:
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Produs.self]
}
@Model
class Produs {
var nume: String
var pret: Double
init(nume: String, pret: Double) {
self.nume = nume
self.pret = pret
}
}
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Produs.self]
}
@Model
class Produs {
var nume: String
var pret: Double
var moneda: String
init(nume: String, pret: Double, moneda: String = "RON") {
self.nume = nume
self.pret = pret
self.moneda = moneda
}
}
}
enum PlanMigrare: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrareV1laV2]
}
static let migrareV1laV2 = MigrationStage.lightweight(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
)
}
Moștenirea Claselor (iOS 26 / WWDC 2025)
Cea mai nouă adăugire la SwiftData e suportul pentru moștenirea claselor, introdus în iOS 26. Dacă ați lucrat cu entitățile părinte/copil din Core Data, conceptul vă e familiar:
@Model
class ElementMedia {
var titlu: String
var dataAdaugarii: Date
init(titlu: String) {
self.titlu = titlu
self.dataAdaugarii = Date()
}
}
@Model
class Film: ElementMedia {
var durata: Int // minute
var regizor: String
init(titlu: String, durata: Int, regizor: String) {
self.durata = durata
self.regizor = regizor
super.init(titlu: titlu)
}
}
@Model
class Carte: ElementMedia {
var numarPagini: Int
var autor: String
init(titlu: String, numarPagini: Int, autor: String) {
self.numarPagini = numarPagini
self.autor = autor
super.init(titlu: titlu)
}
}
Cele Mai Bune Practici și Sfaturi de Performanță
Sfaturi pentru Performanță
- Folosiți indexuri (
#Index) pe proprietățile interogate frecvent — diferența pe seturi mari de date e dramatică - Filtrați la nivel de bază de date, nu în memorie — un
#Predicatee executat la nivel SQLite, mult mai eficient decât filtrarea unui array în Swift - Folosiți
fetchCount()când aveți nevoie doar de numărul de rezultate, nu de obiectele în sine - Stocați datele mari extern cu
@Attribute(.externalStorage)— imaginile și fișierele nu ar trebui stocate direct în baza de date - Evitați interogările în bucle — preîncărcați datele necesare într-o singură interogare
Pattern-uri Arhitecturale
Deși SwiftData funcționează excelent direct în view-uri, pentru aplicații mai mari recomand un pattern de tip repository care separă logica de date de interfață:
@Observable
class ProdusRepository {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func toateProdusele() throws -> [Produs] {
let descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\Produs.nume)]
)
return try modelContext.fetch(descriptor)
}
func adaugaProdus(_ produs: Produs) {
modelContext.insert(produs)
}
func stergeProdus(_ produs: Produs) {
modelContext.delete(produs)
}
func cautaProduse(text: String) throws -> [Produs] {
let predicat = #Predicate { produs in
produs.nume.localizedStandardContains(text)
}
let descriptor = FetchDescriptor(predicate: predicat)
return try modelContext.fetch(descriptor)
}
}
Erori Frecvente de Evitat
- Nu stocați array-uri de tipuri primitive (gen
[String]) dacă aveți nevoie să le căutați — SwiftData le codifică ca blob-uri binare, imposibil de interogat. Creați un model separat cu o relație - Nu inserați obiectele relaționate separat — lăsați SwiftData să le insereze automat, altfel veți obține erori de înregistrare duplicată
- Nu uitați relațiile inverse când folosiți CloudKit — sincronizarea eșuează fără ele, și nu veți primi nicio eroare
- Nu subclasați modelele pe iOS 17 — moștenirea e suportată abia din iOS 26
Întrebări Frecvente (FAQ)
SwiftData înlocuiește complet Core Data?
Nu încă, dar Apple se îndreaptă clar în această direcție. Core Data rămâne necesar dacă trebuie să suportați versiuni de iOS anterioare versiunii 17, dacă aveți nevoie de NSFetchedResultsController sau predicate compuse complexe, sau dacă aplicația folosește partajarea CloudKit. Pentru proiecte noi care vizează iOS 17+, SwiftData e alegerea de bază.
Pot folosi SwiftData și Core Data în același timp?
Da, și chiar e recomandat pentru migrări treptate. Cele două framework-uri pot partaja aceeași bază de date și rămân sincronizate. Asigurați-vă doar că modificările de schemă sunt aplicate simultan în ambele modele de date.
Cum funcționează SwiftData cu previzualizările Xcode?
Folosiți un ModelContainer configurat cu isStoredInMemoryOnly: true pentru previzualizări. Creează o bază de date temporară în memorie care dispare la închiderea previzualizării — perfect pentru teste cu date de exemplu.
SwiftData funcționează cu UIKit?
Absolut. SwiftData nu e limitat la SwiftUI. Puteți crea un ModelContainer programatic și obține un ModelContext pentru operații CRUD în orice context — controllere UIKit, servicii de background sau extensii de aplicație.
Ce se întâmplă dacă modific schema după ce utilizatorii au date existente?
SwiftData gestionează automat migrările lightweight — adăugarea de proprietăți cu valori implicite, ștergerea sau redenumirea lor. Pentru schimbări mai complexe (modificarea tipului unei proprietăți, restructurarea relațiilor), trebuie să definiți un plan de migrare explicit cu VersionedSchema și SchemaMigrationPlan.