SwiftData od základov po produkciu: Kompletný sprievodca modernou dátovou vrstvou v Swift

Praktický sprievodca frameworkom SwiftData — od definície modelov cez @Model makro, CRUD operácie a vzťahy, až po migrácie schém, CloudKit synchronizáciu a novinky v iOS 26 vrátane dedičnosti modelov.

Úvod: Prečo SwiftData mení pravidlá hry

Ak ste niekedy pracovali s Core Data, tak presne viete, o čom hovorím. XML schémy, NSManagedObject podtriedy, NSFetchRequest s magickými reťazcami, NSPersistentContainer... Fungovalo to, jasné — ale vyžadovalo si to hromadu boilerplate kódu a pomerne hlboké pochopenie interných mechanizmov. Na WWDC 2023 Apple konečne predstavil SwiftData — framework, ktorý radikálne zjednodušuje prácu s dátovou vrstvou v Swift aplikáciách.

A nie, SwiftData nie je len „nový kabát" pre Core Data. Je to úplne nové API postavené na princípoch moderného Swiftu — makrá, property wrappery, natívna integrácia so SwiftUI a podpora pre Swift concurrency. Pod kapotou síce stále využíva overenú infraštruktúru Core Data (SQLite, persistent store coordinator), ale vy s ňou nikdy priamo neprichádzate do styku. Čo je, úprimne povedané, obrovská úľava.

V tomto článku prejdeme celým životným cyklom SwiftData — od definície modelov, cez CRUD operácie, vzťahy a migrácie, až po CloudKit synchronizáciu a novinky z iOS 26. Každú sekciu doplníme praktickými príkladmi kódu, ktoré môžete rovno použiť vo vašich projektoch. Tak poďme na to.

@Model makro: Základ všetkého

Srdcom SwiftData je makro @Model. Namiesto vytvárania .xcdatamodeld súborov a generovania NSManagedObject podtried jednoducho označíte svoju Swift triedu týmto makrom a SwiftData sa postará o zvyšok. Seriózne — je to naozaj tak jednoduché.

Základná definícia modelu

import SwiftData

@Model
final class Article {
    var title: String
    var content: String
    var publishedDate: Date
    var viewCount: Int
    var isDraft: Bool

    init(title: String, content: String, publishedDate: Date = .now, viewCount: Int = 0, isDraft: Bool = true) {
        self.title = title
        self.content = content
        self.publishedDate = publishedDate
        self.viewCount = viewCount
        self.isDraft = isDraft
    }
}

To je naozaj všetko. Žiadne XML, žiadne generované súbory. Makro @Model za vás vygeneruje potrebnú infraštruktúru — conformance k PersistentModel protokolu, sledovanie zmien (observation) a mapovanie na SQLite stĺpce. Keď si to porovnáte s tým, čo bolo treba v Core Data, je to noc a deň.

Podporované typy

SwiftData natívne podporuje tieto typy vlastností:

  • Základné typy: String, Int, Double, Float, Bool, Date, Data, URL, UUID
  • Voliteľné typy: Akýkoľvek z vyššie uvedených zabalený v Optional
  • Kolekcie: Array podporovaných typov
  • Enumerácie: Ak implementujú Codable
  • Vlastné Codable štruktúry: Uložené ako transformovateľné atribúty

Atribúty a ich konfigurácia

Pre jemnejšiu kontrolu nad správaním vlastností používame makro @Attribute:

@Model
final class User {
    @Attribute(.unique) var email: String
    @Attribute(.externalStorage) var profileImage: Data?
    @Attribute(.spotlight) var displayName: String
    @Attribute(.encrypt) var sensitiveNote: String?

    // Transformovateľný atribút - vlastná Codable štruktúra
    var preferences: UserPreferences

    // Transientný atribút - neukladá sa do databázy
    @Transient var isOnline: Bool = false

    init(email: String, displayName: String, preferences: UserPreferences = .default) {
        self.email = email
        self.displayName = displayName
        self.preferences = preferences
    }
}

struct UserPreferences: Codable {
    var theme: String
    var fontSize: Int
    var notificationsEnabled: Bool

    static let `default` = UserPreferences(
        theme: "system",
        fontSize: 16,
        notificationsEnabled: true
    )
}

Atribút .unique je obzvlášť dôležitý — zabezpečuje, že hodnota bude v databáze unikátna. A tu je zaujímavá vec: ak sa pokúsite vložiť duplicitnú hodnotu, SwiftData automaticky vykoná upsert — aktualizuje existujúci záznam namiesto vytvorenia nového. Toto správanie je kľúčové pri synchronizácii dát a osobne si myslím, že je to jeden z najlepších designových rozhodnutí frameworku.

ModelContainer a ModelContext: Dátový zásobník

V Core Data ste museli konfigurovať NSPersistentContainer, vytvárať NSManagedObjectContext a riešiť koordináciu medzi nimi. SwiftData toto dramaticky zjednodušuje pomocou dvoch kľúčových tried: ModelContainer a ModelContext.

Nastavenie v SwiftUI aplikácii

Najjednoduchší spôsob je použiť modifikátor .modelContainer priamo v App štruktúre:

import SwiftUI
import SwiftData

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Article.self, User.self])
    }
}

Jeden riadok. Jeden jediný riadok a máte kompletný dátový zásobník — SQLite databázu, kontajner aj kontext — sprístupnený celej hierarchii SwiftUI views cez environment. Ak ste strávili hodiny konfiguráciou Core Data stacku, toto vás poteší.

Pokročilá konfigurácia

Pre produkčné aplikácie budete často potrebovať väčšiu kontrolu nad konfiguráciou:

@main
struct MyApp: App {
    let container: ModelContainer

    init() {
        let schema = Schema([Article.self, User.self])

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

        do {
            container = try ModelContainer(
                for: schema,
                configurations: [config]
            )
        } catch {
            fatalError("Failed to initialize ModelContainer: \(error)")
        }
    }

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

Práca s ModelContext

ModelContext je váš hlavný nástroj pre interakciu s dátami. V SwiftUI views ho získate cez environment:

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

    // ...
}

Mimo SwiftUI (napríklad v servisných triedach) si môžete vytvoriť nový kontext z kontajnera:

class ArticleService {
    private let modelContext: ModelContext

    init(container: ModelContainer) {
        self.modelContext = ModelContext(container)
        self.modelContext.autosaveEnabled = false
    }

    func performBatchImport(articles: [ArticleDTO]) throws {
        for dto in articles {
            let article = Article(title: dto.title, content: dto.content)
            modelContext.insert(article)
        }
        try modelContext.save()
    }
}

Jeden dôležitý detail: ModelContext má predvolene zapnuté autosaveEnabled, takže zmeny sa automaticky ukladajú pri vhodných príležitostiach (napríklad keď aplikácia prejde do pozadia). Pre dávkové operácie je ale lepšie autosave vypnúť a volať save() manuálne — inak by vám mohli vzniknúť nekonzistentné stavy.

@Query: Dáta priamo vo SwiftUI views

Property wrapper @Query je podľa mňa jednou z najelegantnejších častí SwiftData. Deklaratívne definujete, aké dáta chcete, a SwiftUI view sa automaticky aktualizuje pri každej zmene. Žiadne manuálne refreshovanie, žiadne NotificationCenter.

Základné použitie

struct ArticleListView: View {
    @Query var articles: [Article]

    var body: some View {
        List(articles) { article in
            ArticleRow(article: article)
        }
    }
}

Triedenie a filtrovanie

// Triedenie podľa dátumu publikovania (zostupne)
@Query(sort: \.publishedDate, order: .reverse)
var articles: [Article]

// Viacúrovňové triedenie
@Query(sort: [
    SortDescriptor(\.isDraft, order: .forward),
    SortDescriptor(\.publishedDate, order: .reverse)
])
var articles: [Article]

// Filtrovanie s predikátom
@Query(filter: #Predicate
{ article in article.isDraft == false && article.viewCount > 100 }) var popularArticles: [Article] // Limitovanie výsledkov @Query(sort: \.viewCount, order: .reverse) var topArticles: [Article]

Dynamické query

Často potrebujete meniť parametre query za behu — napríklad pri vyhľadávaní. Toto je miesto, kde SwiftData naozaj žiari. Riešenie je cez inicializátor:

struct ArticleSearchView: View {
    @Query var articles: [Article]

    init(searchText: String, showDraftsOnly: Bool) {
        let predicate = #Predicate
{ article in (searchText.isEmpty || article.title.localizedStandardContains(searchText)) && (!showDraftsOnly || article.isDraft == true) } _articles = Query( filter: predicate, sort: \.publishedDate, order: .reverse ) } var body: some View { List(articles) { article in ArticleRow(article: article) } } }

Rodičovský view potom jednoducho vytvorí inštanciu s aktuálnymi parametrami:

struct ContentView: View {
    @State private var searchText = ""
    @State private var showDraftsOnly = false

    var body: some View {
        NavigationStack {
            ArticleSearchView(
                searchText: searchText,
                showDraftsOnly: showDraftsOnly
            )
            .searchable(text: $searchText)
            .toolbar {
                Toggle("Len koncepty", isOn: $showDraftsOnly)
            }
        }
    }
}

CRUD operácie: Vytvorenie, čítanie, aktualizácia, mazanie

Práca s dátami v SwiftData je intuitívna a priamočiara. Pozrime sa na všetky štyri základné operácie — žiadne prekvapenia, len čistý a jasný kód.

Create — Vytvorenie záznamu

struct CreateArticleView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss

    @State private var title = ""
    @State private var content = ""

    var body: some View {
        Form {
            TextField("Názov článku", text: $title)
            TextEditor(text: $content)

            Button("Uložiť") {
                let article = Article(
                    title: title,
                    content: content
                )
                modelContext.insert(article)
                // Autosave sa postará o uloženie
                dismiss()
            }
        }
    }
}

Read — Čítanie dát

Okrem @Query property wrappera môžete dáta načítať aj imperatívne pomocou FetchDescriptor:

func fetchRecentArticles() throws -> [Article] {
    var descriptor = FetchDescriptor
( predicate: #Predicate { $0.isDraft == false }, sortBy: [SortDescriptor(\.publishedDate, order: .reverse)] ) descriptor.fetchLimit = 10 return try modelContext.fetch(descriptor) } // Počítanie záznamov bez ich načítania func countPublishedArticles() throws -> Int { let descriptor = FetchDescriptor
( predicate: #Predicate { $0.isDraft == false } ) return try modelContext.fetchCount(descriptor) }

Update — Aktualizácia

Aktualizácia je v SwiftData neuveriteľne jednoduchá — stačí zmeniť vlastnosť objektu a hotovo. SwiftData automaticky sleduje a uloží zmenu. Žiadne setValue:forKey:, žiadne explicitné oznámenie o zmene:

struct ArticleDetailView: View {
    @Bindable var article: Article

    var body: some View {
        Form {
            TextField("Názov", text: $article.title)
            TextEditor(text: $article.content)

            Toggle("Koncept", isOn: $article.isDraft)

            Button("Publikovať") {
                article.isDraft = false
                article.publishedDate = .now
                // Zmeny sa automaticky uložia
            }
        }
    }
}

Všimnite si použitie @Bindable — tento property wrapper umožňuje vytvárať bindings priamo na vlastnosti @Model objektov. Funguje to vďaka tomu, že @Model automaticky pridáva conformance k Observable protokolu. Elegantné riešenie.

Delete — Mazanie

struct ArticleListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \.publishedDate, order: .reverse) var articles: [Article]

    var body: some View {
        List {
            ForEach(articles) { article in
                ArticleRow(article: article)
            }
            .onDelete(perform: deleteArticles)
        }
    }

    private func deleteArticles(at offsets: IndexSet) {
        for index in offsets {
            modelContext.delete(articles[index])
        }
    }
}

// Hromadné mazanie
func deleteAllDrafts() throws {
    try modelContext.delete(
        model: Article.self,
        where: #Predicate { $0.isDraft == true }
    )
}

Vzťahy medzi modelmi

Reálne aplikácie málokedy pracujú s izolovanými entitami — a tu prichádza na scénu makro @Relationship. SwiftData poskytuje elegantnú syntax pre definovanie vzťahov medzi modelmi.

One-to-Many (Jeden k viacerým)

@Model
final class Author {
    var name: String
    var bio: String?

    @Relationship(deleteRule: .cascade, inverse: \Article.author)
    var articles: [Article] = []

    init(name: String, bio: String? = nil) {
        self.name = name
        self.bio = bio
    }
}

@Model
final class Article {
    var title: String
    var content: String
    var publishedDate: Date

    var author: Author?

    init(title: String, content: String, publishedDate: Date = .now) {
        self.title = title
        self.content = content
        self.publishedDate = publishedDate
    }
}

Parameter deleteRule: .cascade znamená, že keď zmažete autora, všetky jeho články sa automaticky zmažú tiež. Tu sú všetky dostupné pravidlá mazania:

  • .cascade: Zmazanie rodiča zmaže aj všetky deti
  • .nullify: Zmazanie rodiča nastaví referenciu na nil (predvolené)
  • .deny: Zabráni zmazaniu rodiča, ak má deti
  • .noAction: Žiadna automatická akcia (pozor na osirelé záznamy!)

Many-to-Many (Viacerí k viacerým)

@Model
final class Article {
    var title: String
    var content: String

    @Relationship(inverse: \Tag.articles)
    var tags: [Tag] = []

    init(title: String, content: String) {
        self.title = title
        self.content = content
    }
}

@Model
final class Tag {
    @Attribute(.unique) var name: String
    var articles: [Article] = []

    init(name: String) {
        self.name = name
    }
}

Práca so vzťahmi je potom veľmi prirodzená — vlastne len pracujete s bežnými Swift poľami:

// Priradenie tagov k článku
let swiftTag = Tag(name: "Swift")
let tutorialTag = Tag(name: "Tutorial")
modelContext.insert(swiftTag)
modelContext.insert(tutorialTag)

let article = Article(title: "SwiftData Guide", content: "...")
article.tags = [swiftTag, tutorialTag]
modelContext.insert(article)

// Prístup k vzťahom
for tag in article.tags {
    print("Tag: \(tag.name)")
}

// Spätný vzťah
for article in swiftTag.articles {
    print("Article: \(article.title)")
}

One-to-One (Jeden k jednému)

@Model
final class User {
    var name: String

    @Relationship(deleteRule: .cascade, inverse: \UserProfile.user)
    var profile: UserProfile?

    init(name: String) {
        self.name = name
    }
}

@Model
final class UserProfile {
    var avatarURL: URL?
    var biography: String?
    var websiteURL: URL?

    var user: User?

    init(avatarURL: URL? = nil, biography: String? = nil) {
        self.avatarURL = avatarURL
        self.biography = biography
    }
}

Predikáty a FetchDescriptor: Pokročilé filtrovanie

Makro #Predicate je jednou z najsilnejších stránok SwiftData. Ak ste niekedy písali NSPredicate s jeho reťazcovými formátmi a potom hľadali preklepy v runtime, toto vás nadchne — #Predicate poskytuje plnú typovú bezpečnosť a kontrolu už v čase kompilácie.

Základné predikáty

// Jednoduché porovnanie
let published = #Predicate
{ $0.isDraft == false } // Hľadanie v texte let searchPredicate = #Predicate
{ article in article.title.localizedStandardContains("Swift") } // Porovnanie dátumov let lastWeek = Calendar.current.date(byAdding: .day, value: -7, to: .now)! let recentPredicate = #Predicate
{ article in article.publishedDate > lastWeek }

Zložené predikáty

// Kombinácia viacerých podmienok
let minViews = 100
let complexPredicate = #Predicate
{ article in article.isDraft == false && article.viewCount >= minViews && article.title.localizedStandardContains("Swift") } // Predikát s voliteľnými hodnotami let authorName = "Jan Novák" let authorPredicate = #Predicate
{ article in article.author?.name == authorName }

FetchDescriptor — plná kontrola nad dopytmi

func fetchArticles(
    searchText: String,
    category: String?,
    page: Int,
    pageSize: Int = 20
) throws -> [Article] {
    let predicate = #Predicate
{ article in article.isDraft == false && (searchText.isEmpty || article.title.localizedStandardContains(searchText)) } var descriptor = FetchDescriptor
( predicate: predicate, sortBy: [ SortDescriptor(\.publishedDate, order: .reverse) ] ) // Stránkovanie descriptor.fetchOffset = page * pageSize descriptor.fetchLimit = pageSize // Optimalizácia - prefetch vzťahov descriptor.relationshipKeyPathsForPrefetching = [\.author, \.tags] return try modelContext.fetch(descriptor) }

Parameter relationshipKeyPathsForPrefetching si zaslúži špeciálnu pozornosť. Bez neho SwiftData načítava vzťahy lenivo (lazy loading), čo môže viesť k notoricky známemu problému N+1 dopytov. S prefetchingom sa všetky potrebné vzťahy načítajú v jednom dotaze — a výkon je rádovo lepší.

Enumerácia výsledkov pre veľké datasety

func processAllArticles() throws {
    let descriptor = FetchDescriptor
() // Namiesto načítania všetkých záznamov do pamäte // ich spracujeme po dávkach let batchSize = 100 var offset = 0 while true { var batchDescriptor = descriptor batchDescriptor.fetchOffset = offset batchDescriptor.fetchLimit = batchSize let batch = try modelContext.fetch(batchDescriptor) if batch.isEmpty { break } for article in batch { // Spracovanie článku article.viewCount += 1 } try modelContext.save() offset += batchSize } }

Verzionovanie schémy a migrácie

Každá aplikácia sa vyvíja a s ňou aj dátový model. To je fakt, ktorému sa nevyhnete. Našťastie SwiftData poskytuje robustný systém pre správu zmien schémy pomocou VersionedSchema a SchemaMigrationPlan.

Definícia verzionovaných schém

// Verzia 1 - pôvodná schéma
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

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

    @Model
    final class Article {
        var title: String
        var content: String
        var createdAt: Date

        init(title: String, content: String) {
            self.title = title
            self.content = content
            self.createdAt = .now
        }
    }
}

// Verzia 2 - pridanie nových vlastností
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

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

    @Model
    final class Article {
        var title: String
        var content: String
        var createdAt: Date
        var publishedDate: Date?
        var viewCount: Int
        var isDraft: Bool

        init(title: String, content: String) {
            self.title = title
            self.content = content
            self.createdAt = .now
            self.publishedDate = nil
            self.viewCount = 0
            self.isDraft = true
        }
    }
}

Migračný plán

enum ArticleMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

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

    // Lightweight migrácia - SwiftData zvládne automaticky
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )
}

Vlastná (custom) migrácia

Niekedy automatická migrácia nestačí — napríklad keď potrebujete transformovať existujúce dáta. V takom prípade použijete custom migráciu:

enum SchemaV3: VersionedSchema {
    static var versionIdentifier = Schema.Version(3, 0, 0)

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

    @Model
    final class Article {
        var title: String
        var content: String
        var createdAt: Date
        var publishedDate: Date?
        var viewCount: Int
        var isDraft: Bool
        var slug: String  // Nové pole - URL-friendly verzia názvu

        init(title: String, content: String) {
            self.title = title
            self.content = content
            self.createdAt = .now
            self.slug = title.lowercased()
                .replacingOccurrences(of: " ", with: "-")
        }
    }
}

enum ArticleMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }

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

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )

    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SchemaV2.self,
        toVersion: SchemaV3.self,
        willMigrate: nil,
        didMigrate: { context in
            // Po migrácii schémy naplníme slug pre existujúce články
            let articles = try context.fetch(
                FetchDescriptor()
            )

            for article in articles {
                article.slug = article.title.lowercased()
                    .replacingOccurrences(of: " ", with: "-")
            }

            try context.save()
        }
    )
}

Použitie migračného plánu

@main
struct MyApp: App {
    let container: ModelContainer

    init() {
        do {
            container = try ModelContainer(
                for: SchemaV3.Article.self,
                migrationPlan: ArticleMigrationPlan.self
            )
        } catch {
            fatalError("Migration failed: \(error)")
        }
    }

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

Kľúčové pravidlo, ktoré si zapamätajte: nikdy nemodifikujte existujúce verzie schémy. Vždy vytvorte novú verziu. Tie staré slúžia ako historický záznam, podľa ktorého SwiftData vie, ako migrovať dáta vašich používateľov. Porušenie tohto pravidla vás bude stáť nespočetné hodiny debugovania.

CloudKit synchronizácia

Jednou z veľkých výhod SwiftData je vstavaná podpora pre iCloud synchronizáciu. Vaše dáta sa automaticky synchronizujú medzi zariadeniami používateľa bez potreby písať sieťový kód. Znie to takmer príliš dobre? No, je tu pár háčikov.

Nastavenie CloudKit synchronizácie

Najprv musíte v Xcode nakonfigurovať projekt:

  1. Pridajte capability iCloud v cieľovom nastavení projektu
  2. Zaškrtnite CloudKit
  3. Vytvorte alebo vyberte CloudKit kontajner
  4. Pridajte capability Background Modes a zaškrtnite Remote notifications
let config = ModelConfiguration(
    "CloudStore",
    cloudKitDatabase: .private("iCloud.com.myapp.data")
)

let container = try ModelContainer(
    for: Article.self, Author.self, Tag.self,
    configurations: [config]
)

Obmedzenia pri CloudKit synchronizácii

A tu sú tie spomínané háčiky. CloudKit so SwiftData má niekoľko dôležitých obmedzení, o ktorých by ste mali vedieť ešte predtým, než sa do toho pustíte:

  • Žiadne unique constraints: Atribút .unique nie je kompatibilný s CloudKit. Deduplikáciu musíte riešiť manuálne.
  • Všetky vlastnosti musia byť voliteľné alebo mať predvolenú hodnotu: CloudKit záznamy môžu prísť neúplné.
  • Žiadne pravidlo .deny: Delete rule .deny nie je podporované.
  • Len privátna databáza: Pre zdieľanie dát medzi používateľmi potrebujete CKShare.

CloudKit-kompatibilný model

@Model
final class Article {
    // Všetky vlastnosti majú predvolenú hodnotu
    var title: String = ""
    var content: String = ""
    var publishedDate: Date = Date.now
    var viewCount: Int = 0
    var isDraft: Bool = true

    // Vzťahy sú voliteľné
    @Relationship(deleteRule: .nullify, inverse: \Tag.articles)
    var tags: [Tag] = []

    var author: Author?

    init(title: String, content: String) {
        self.title = title
        self.content = content
    }
}

Hybridný prístup: lokálne a cloudové dáta

Niekedy chcete niektoré dáta synchronizovať a iné ponechať len lokálne. Dobrá správa — SwiftData to umožňuje cez viacero konfigurácií:

let cloudConfig = ModelConfiguration(
    "CloudStore",
    schema: Schema([Article.self, Author.self]),
    cloudKitDatabase: .private("iCloud.com.myapp")
)

let localConfig = ModelConfiguration(
    "LocalStore",
    schema: Schema([UserSettings.self, CacheEntry.self]),
    cloudKitDatabase: .none
)

let container = try ModelContainer(
    for: Article.self, Author.self, UserSettings.self, CacheEntry.self,
    configurations: [cloudConfig, localConfig]
)

Každý model musí patriť presne do jednej konfigurácie. SwiftData automaticky smeruje operácie do správneho úložiska podľa typu modelu — o toto sa nemusíte starať.

Novinky v iOS 26 (WWDC 2025)

Na WWDC 2025 Apple priniesol niekoľko významných vylepšení SwiftData. A treba povedať, že riešia presne tie veci, ktoré komunita najhlasnejšie požadovala.

Dedičnosť modelov (Model Inheritance)

Toto bola jedna z najočakávanejších funkcií — a konečne je tu. Pred iOS 26 ste museli používať kompozíciu alebo protokoly na simulovanie hierarchie modelov. Teraz môžete priamo dediť:

@Model
class MediaItem {
    var title: String
    var createdAt: Date
    var fileSize: Int

    init(title: String, fileSize: Int) {
        self.title = title
        self.createdAt = .now
        self.fileSize = fileSize
    }
}

@Model
final class Photo: MediaItem {
    var width: Int
    var height: Int
    var camera: String?

    init(title: String, fileSize: Int, width: Int, height: Int) {
        self.width = width
        self.height = height
        super.init(title: title, fileSize: fileSize)
    }
}

@Model
final class Video: MediaItem {
    var duration: TimeInterval
    var resolution: String

    init(title: String, fileSize: Int, duration: TimeInterval, resolution: String) {
        self.duration = duration
        self.resolution = resolution
        super.init(title: title, fileSize: fileSize)
    }
}

Dotazy na rodičovskú triedu automaticky vracajú aj inštancie podtried, čo je presne to správanie, aké by ste očakávali:

// Vráti Photo aj Video objekty
@Query var allMedia: [MediaItem]

// Vráti len fotografie
@Query var photos: [Photo]

Vylepšené History API

iOS 26 prináša výrazne vylepšené API pre sledovanie histórie zmien v databáze. Toto je obzvlášť užitočné pre synchronizáciu, audit log a undo/redo funkcionalitu:

// Získanie histórie zmien od posledného spracovania
func processChanges(since token: DefaultHistoryToken?) throws -> DefaultHistoryToken? {
    var descriptor = HistoryDescriptor()
    if let token {
        descriptor.predicate = #Predicate { transaction in
            transaction.token > token
        }
    }

    let transactions = try modelContext.fetchHistory(descriptor)

    for transaction in transactions {
        for change in transaction.changes {
            switch change {
            case .insert(let inserted):
                print("Inserted: \(inserted.changedPersistentIdentifier)")
            case .update(let updated):
                print("Updated: \(updated.changedPersistentIdentifier)")
            case .delete(let deleted):
                print("Deleted: \(deleted.changedPersistentIdentifier)")
            }
        }
    }

    return transactions.last?.token
}

Vylepšené predikáty a výrazy

SwiftData v iOS 26 rozšíruje možnosti #Predicate makra o ďalšie operácie, vrátane lepšej podpory pre prácu s kolekciami vo vzťahoch:

// Filtrovanie podľa počtu vzťahov
let prolificAuthors = #Predicate { author in
    author.articles.count > 10
}

// Filtrovanie cez vnorené vzťahy
let articlesWithSwiftTag = #Predicate
{ article in article.tags.contains(where: { $0.name == "Swift" }) }

Best practices a časté chyby

Po niekoľkých rokoch, čo je SwiftData v produkčnom nasadení, sa začínajú kryštalizovať osvedčené postupy a typické úskalia. Poďme si prejsť tie najdôležitejšie.

Výkonnostné tipy

1. Používajte fetchLimit a fetchOffset

Nikdy nenačítavajte všetky záznamy, ak ich všetky nepotrebujete. Stránkovanie je základ výkonnej aplikácie:

var descriptor = FetchDescriptor
( sortBy: [SortDescriptor(\.publishedDate, order: .reverse)] ) descriptor.fetchLimit = 20 descriptor.fetchOffset = page * 20

2. Prefetchujte vzťahy

Ak viete, že budete pristupovať k vzťahom, prefetchnite ich. Vyhnete sa tak N+1 problému, ktorý vám môže poriadne spomaliť aplikáciu:

descriptor.relationshipKeyPathsForPrefetching = [\.author, \.tags]

3. Používajte fetchCount namiesto fetch().count

// Zlé - načíta všetky objekty do pamäte
let count = try modelContext.fetch(descriptor).count

// Dobré - spočíta na úrovni SQL
let count = try modelContext.fetchCount(descriptor)

Tento rozdiel sa môže zdať malý, ale pri tisíckach záznamov je obrovský.

4. Dávkové operácie robte v samostatnom kontexte

func importArticles(_ dtos: [ArticleDTO], container: ModelContainer) throws {
    let backgroundContext = ModelContext(container)
    backgroundContext.autosaveEnabled = false

    for dto in dtos {
        let article = Article(title: dto.title, content: dto.content)
        backgroundContext.insert(article)
    }

    try backgroundContext.save()
}

5. Používajte @Transient pre vypočítané vlastnosti

@Model
final class Article {
    var title: String
    var content: String

    @Transient
    var wordCount: Int {
        content.split(separator: " ").count
    }

    @Transient
    var readingTime: TimeInterval {
        Double(wordCount) / 200.0 * 60.0 // 200 slov za minútu
    }
}

Časté chyby a ako sa im vyhnúť

Chyba 1: Používanie modelov mimo ich kontextu

Model objekty sú viazané na ModelContext, v ktorom boli vytvorené alebo načítané. Prístup k nim z iného vlákna alebo po zmazaní kontextu môže viesť k pádom aplikácie. Toto je klasická chyba, na ktorú narazí skoro každý:

// NEROBTE TOTO
Task.detached {
    let article = articles.first! // Prístup z iného vlákna
    print(article.title) // Potenciálny pád
}

// ROBTE TOTO
let articleID = articles.first!.persistentModelID
Task.detached {
    let context = ModelContext(container)
    if let article = context.model(for: articleID) as? Article {
        print(article.title)
    }
}

Chyba 2: Zabudnutie na predvolené hodnoty pri CloudKit

Toto je jedna z najčastejších chýb a je o to zákernejšia, že synchronizácia zlyhá bez jasného chybového hlásenia. Ak používate CloudKit, všetky vlastnosti jednoducho musia mať predvolenú hodnotu alebo byť voliteľné. Žiadna výnimka.

Chyba 3: Prílišná závislosť na autosave

Autosave je pohodlný, ale nemáte kontrolu nad tým, kedy presne sa zmeny uložia. Pre kritické operácie vždy volajte save() explicitne:

func completePurchase(order: Order) throws {
    order.status = .completed
    order.completedAt = .now

    // Explicitné uloženie - nechceme riskovať stratu dát
    try modelContext.save()
}

Chyba 4: Nesprávne predikáty s voliteľnými hodnotami

// Toto môže spôsobiť problémy
let predicate = #Predicate
{ $0.author?.name == "Jan" } // Bezpečnejšia verzia let predicate = #Predicate
{ article in if let author = article.author { return author.name == "Jan" } return false }

Kedy použiť SwiftData vs Core Data

SwiftData nie je (zatiaľ) univerzálnou náhradou za Core Data. Tu sú scenáre, kde je každá technológia vhodnejšia:

Použite SwiftData, keď:

  • Začínate nový projekt s podporou iOS 17+
  • Vaša aplikácia je primárne SwiftUI
  • Nepotrebujete pokročilé funkcie Core Data (NSFetchedResultsController s oddielmi, abstraktné entity v starších verziách)
  • Chcete rýchly vývoj s minimálnym boilerplate kódom

Zostaňte pri Core Data, keď:

  • Musíte podporovať iOS 16 alebo staršie verzie
  • Máte rozsiahlu existujúcu Core Data infraštruktúru
  • Potrebujete funkcie, ktoré SwiftData zatiaľ nepodporuje
  • Vaša aplikácia používa primárne UIKit s komplexnými NSFetchedResultsController konfiguráciami

Hybridný prístup: A tu je dobrá správa — SwiftData a Core Data môžu koexistovať v rovnakej aplikácii. Môžete zdieľať rovnaký SQLite súbor a postupne migrovať z Core Data na SwiftData:

let coreDataURL = NSPersistentContainer
    .defaultDirectoryURL()
    .appendingPathComponent("MyApp.sqlite")

let config = ModelConfiguration(url: coreDataURL)
let container = try ModelContainer(
    for: Article.self,
    configurations: [config]
)

Testovanie s SwiftData

SwiftData výborne podporuje unit testovanie vďaka in-memory konfigurácii. A to je veľké plus, pretože testovanie dátovej vrstvy bolo s Core Data vždy trochu bolestivé:

final class ArticleTests: XCTestCase {
    var container: ModelContainer!
    var context: ModelContext!

    override func setUp() {
        super.setUp()

        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        container = try! ModelContainer(
            for: Article.self, Author.self, Tag.self,
            configurations: [config]
        )
        context = ModelContext(container)
    }

    override func tearDown() {
        container = nil
        context = nil
        super.tearDown()
    }

    func testCreateArticle() throws {
        let article = Article(
            title: "Test Article",
            content: "Test content"
        )
        context.insert(article)
        try context.save()

        let descriptor = FetchDescriptor
() let articles = try context.fetch(descriptor) XCTAssertEqual(articles.count, 1) XCTAssertEqual(articles.first?.title, "Test Article") } func testCascadeDelete() throws { let author = Author(name: "Test Author") let article1 = Article(title: "Article 1", content: "...") let article2 = Article(title: "Article 2", content: "...") author.articles = [article1, article2] context.insert(author) try context.save() // Zmazanie autora by malo zmazať aj články context.delete(author) try context.save() let articles = try context.fetch(FetchDescriptor
()) XCTAssertEqual(articles.count, 0) } func testPredicateFiltering() throws { let draft = Article(title: "Draft", content: "...") draft.isDraft = true let published = Article(title: "Published", content: "...") published.isDraft = false context.insert(draft) context.insert(published) try context.save() let descriptor = FetchDescriptor
( predicate: #Predicate { $0.isDraft == false } ) let results = try context.fetch(descriptor) XCTAssertEqual(results.count, 1) XCTAssertEqual(results.first?.title, "Published") } }

In-memory konfigurácia zabezpečuje, že každý test beží s čistou databázou a testy sa navzájom neovplyvňujú. Plus sú testy výrazne rýchlejšie, pretože sa nič nezapisuje na disk.

Architektúra a organizácia kódu

Pre väčšie projekty odporúčam oddeliť dátovú vrstvu do samostatných modulov. Nie je to povinné, ale z dlhodobého hľadiska sa vám to vráti:

// DataLayer/Models/Article.swift
@Model
final class Article {
    var title: String
    var content: String
    var publishedDate: Date
    var isDraft: Bool

    init(title: String, content: String) {
        self.title = title
        self.content = content
        self.publishedDate = .now
        self.isDraft = true
    }
}

// DataLayer/Services/ArticleRepository.swift
@Observable
final class ArticleRepository {
    private let modelContext: ModelContext

    init(modelContext: ModelContext) {
        self.modelContext = modelContext
    }

    func fetchPublished(limit: Int = 20) throws -> [Article] {
        var descriptor = FetchDescriptor
( predicate: #Predicate { $0.isDraft == false }, sortBy: [SortDescriptor(\.publishedDate, order: .reverse)] ) descriptor.fetchLimit = limit return try modelContext.fetch(descriptor) } func create(title: String, content: String) -> Article { let article = Article(title: title, content: content) modelContext.insert(article) return article } func delete(_ article: Article) { modelContext.delete(article) } func save() throws { try modelContext.save() } }

Tento vzor vám umožní ľahko testovať biznis logiku, vymeniť implementáciu úložiska (napríklad za mock pre testy) a udržiavať SwiftUI views čisté a zamerané čisto na prezentáciu.

Záver

SwiftData predstavuje zásadný posun v tom, ako pristupujeme k dátovej perzistencii v Apple ekosystéme. Framework úspešne abstrahuje komplexnosť Core Data do intuitívneho, typovo bezpečného API, ktoré sa prirodzene integruje so SwiftUI a moderným Swiftom.

Zhrňme si, čo sme prešli:

  • @Model makro eliminuje potrebu XML schém a generovaných podtried — váš dátový model je čistý Swift kód
  • ModelContainer a ModelContext zjednodušujú konfiguráciu dátového zásobníka na minimum
  • @Query property wrapper prináša reaktívne načítavanie dát priamo do SwiftUI views
  • #Predicate makro poskytuje typovo bezpečné filtrovanie s kontrolou v čase kompilácie
  • Vzťahy sa definujú prirodzene cez Swift vlastnosti s konfigurovateľnými pravidlami mazania
  • Migrácie sú spravované cez verzionované schémy a migračné plány
  • CloudKit synchronizácia je vstavaná a vyžaduje minimálnu konfiguráciu
  • iOS 26 prináša dedičnosť modelov a vylepšené History API

Ak začínate nový projekt s podporou iOS 17 alebo novšej verzie, SwiftData je podľa mňa jednoznačná voľba pre dátovú vrstvu. Pre existujúce Core Data projekty zvážte postupnú migráciu — oba frameworky môžu bez problémov koexistovať v jednej aplikácii.

Najlepší spôsob, ako sa naučiť SwiftData, je jednoducho začať ho používať. Vytvorte si malý projekt, experimentujte s modelmi, vzťahmi a predikátmi. Rýchlo zistíte, že tá produktivita, ktorú SwiftData prináša, je skutočne transformatívna. Moderná dátová vrstva v Swift nikdy nebola jednoduchšia — a zároveň tak výkonná.