Ghid Complet SwiftData: Persistența Datelor în SwiftUI cu Exemple Practice

Ghid practic SwiftData pentru SwiftUI: configurare, CRUD, relații, predicate avansate, sincronizare iCloud și cele mai bune practici — cu exemple funcționale de cod pentru iOS 18 și iOS 26.

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 @Model foloseș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 categorie a produselor devine nil
  • 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ă:

  1. Deschideți Signing & Capabilities în setările target-ului
  2. Adăugați capabilitatea iCloud și selectați CloudKit
  3. Creați un container CloudKit (de exemplu, iCloud.com.exemplu.aplicatiamea)
  4. 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 #Predicate e 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.

Despre Autor Editorial Team

Our team of expert writers and editors.