Ú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:
Arraypodporovaný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:
- Pridajte capability iCloud v cieľovom nastavení projektu
- Zaškrtnite CloudKit
- Vytvorte alebo vyberte CloudKit kontajner
- 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
.uniquenie 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
.denynie 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á.