Proč SwiftData mění pravidla hry
Pokud jste někdy pracovali s Core Data, víte přesně, o čem mluvím. XML schémata, NSManagedObjectContext, fetch requesty s NSPredicate psané jako řetězce bez kontroly typů — funkční to bylo, ale uživatelsky přívětivé? Ani náhodou. Apple si to naštěstí uvědomil a na WWDC 2023 představil SwiftData — framework, který persistenci dat přetváří od základů s využitím moderních možností jazyka Swift.
SwiftData není jen „nový kabát" pro Core Data. Je to kompletní reimaginace toho, jak by měla práce s perzistentními daty v Swift aplikacích vypadat. Makra, property wrappery, nativní Swift typy — všechno do sebe prostě zapadá. Definice modelů, dotazování i migrace schémat jsou přirozené a typově bezpečné.
V tomhle průvodci si projdeme SwiftData od úplných základů až po pokročilé techniky — včetně dědičnosti modelů v iOS 26, migrace schémat a synchronizace s CloudKit. Tak si připravte Xcode a pojďme na to.
Definice modelů s makrem @Model
Srdcem SwiftData je makro @Model. Zapomeňte na .xcdatamodeld soubory a NSManagedObject podtřídy — stačí označit běžnou Swift třídu a máte perzistentní model:
import SwiftData
@Model
class Article {
var title: String
var content: String
var publishedAt: Date
var isPublished: Bool
var viewCount: Int
init(title: String, content: String, publishedAt: Date = .now, isPublished: Bool = false, viewCount: Int = 0) {
self.title = title
self.content = content
self.publishedAt = publishedAt
self.isPublished = isPublished
self.viewCount = viewCount
}
}
A to je opravdu všechno. Makro @Model za vás vygeneruje veškerou infrastrukturu potřebnou pro persistenci — konformanci k PersistentModel, sledování změn přes Observable a mapování vlastností na databázové sloupce. Žádný boilerplate, žádné ruční mapování. Upřímně, když jsem to viděl poprvé, nemohl jsem uvěřit, jak málo kódu je potřeba.
Atributy a jejich konfigurace
Pro jemnější kontrolu nad tím, jak se vlastnosti ukládají, tu máme makro @Attribute:
@Model
class User {
@Attribute(.unique) var email: String
var name: String
@Attribute(.externalStorage) var avatarData: Data?
@Attribute(.transformable(by: "ColorTransformer")) var favoriteColor: UIColor?
@Attribute(.spotlight) var bio: String?
init(email: String, name: String) {
self.email = email
self.name = name
}
}
Nejdůležitější atributy, které máte k dispozici:
.unique— zajistí unikátnost hodnoty. Při konfliktu SwiftData provede upsert (tedy aktualizaci existujícího záznamu místo vytvoření duplicity).externalStorage— velká data jako obrázky nebo soubory se uloží mimo hlavní databázi, což výrazně pomáhá výkonu.spotlight— indexuje vlastnost pro vyhledávání přes Spotlight.ephemeral— vlastnost se neukládá do databáze, existuje pouze v paměti
Indexy pro rychlejší dotazy
Od iOS 18 můžete definovat indexy pomocí makra #Index. To je fajn novinka, protože správně nastavené indexy dokážou dramaticky zrychlit dotazy nad velkými datovými sadami:
@Model
class Article {
#Index([\.publishedAt], [\.title], [\.isPublished, \.publishedAt])
var title: String
var content: String
var publishedAt: Date
var isPublished: Bool
init(title: String, content: String, publishedAt: Date = .now, isPublished: Bool = false) {
self.title = title
self.content = content
self.publishedAt = publishedAt
self.isPublished = isPublished
}
}
Ten složený index [\.isPublished, \.publishedAt] je obzvlášť šikovný. Pokud často filtrujete publikované články seřazené podle data (a to děláte skoro vždy, ne?), tenhle index vám ušetří spoustu času.
Vztahy mezi modely
Reálné aplikace téměř nikdy nepracují s izolovanými modely. SwiftData podporuje všechny běžné typy vztahů — one-to-one, one-to-many a many-to-many — pomocí makra @Relationship:
@Model
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
class Article {
var title: String
var content: String
var author: Author?
@Relationship(deleteRule: .nullify, inverse: \Tag.articles)
var tags: [Tag]?
init(title: String, content: String, author: Author? = nil) {
self.title = title
self.content = content
self.author = author
}
}
@Model
class Tag {
var name: String
var articles: [Article]?
init(name: String) {
self.name = name
}
}
Klíčovým parametrem je deleteRule, který určuje, co se stane se souvisejícími objekty při smazání:
.cascade— smazání autora smaže i všechny jeho články.nullify— smazání tagu nastaví referenci v článcích nanil.deny— smazání se nezdaří, pokud existují související objekty.noAction— žádná automatická akce (tady si ale dejte pozor na osiřelé záznamy)
Parametr inverse explicitně definuje inverzní vztah. SwiftData si inverzní vztahy dokáže odvodit i sám, ale explicitní definice je prostě bezpečnější — obzvlášť pokud má model více vztahů ke stejnému typu. Lepší být explicitní než potom lovit záhadné bugy.
ModelContainer a ModelContext: Páteř celé architektury
ModelContainer je vstupním bodem do celého SwiftData stacku. Konfiguruje databázi, registruje modely a poskytuje ModelContext pro práci s daty:
import SwiftUI
import SwiftData
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Author.self, Article.self, Tag.self])
}
}
Jeden modifikátor. To je celé. Vytvoří SQLite databázi, zaregistruje všechny modely a automaticky vloží ModelContext do SwiftUI prostředí.
Pro pokročilejší konfiguraci můžete kontejner vytvořit manuálně:
let config = ModelConfiguration(
"MyDatabase",
schema: Schema([Author.self, Article.self, Tag.self]),
isStoredInMemoryOnly: false,
allowsSave: true,
groupContainer: .identifier("group.com.myapp.shared"),
cloudKitDatabase: .automatic
)
let container = try ModelContainer(
for: Author.self, Article.self, Tag.self,
configurations: config
)
Důležitým konceptem je ModelContext. Představte si ho jako „pracovní plochu", na které vytváříte, upravujete a mažete objekty. Změny se neprojeví v databázi, dokud nezavoláte save() — nebo se kontext uloží automaticky, což je mimochodem výchozí chování:
struct ArticleEditorView: View {
@Environment(\.modelContext) private var context
func createArticle() {
let author = Author(name: "Jan Novák")
let article = Article(title: "Nový článek", content: "Obsah...", author: author)
let tag = Tag(name: "Swift")
article.tags = [tag]
context.insert(author)
context.insert(article)
context.insert(tag)
// Uložení je automatické, ale můžete vynutit:
try? context.save()
}
func deleteArticle(_ article: Article) {
context.delete(article)
}
var body: some View {
// ...
}
}
Dotazování dat s @Query
Tady to začíná být opravdu zajímavé. Property wrapper @Query je jednou z nejelegantnějších částí SwiftData. Automaticky načítá data z databáze, sleduje změny a aktualizuje SwiftUI view — a vy nemusíte napsat jediný řádek kódu navíc:
struct ArticleListView: View {
@Query(
filter: #Predicate { $0.isPublished },
sort: [SortDescriptor(\.publishedAt, order: .reverse)],
animation: .default
)
var publishedArticles: [Article]
var body: some View {
List(publishedArticles) { article in
VStack(alignment: .leading, spacing: 4) {
Text(article.title)
.font(.headline)
Text(article.publishedAt, style: .date)
.font(.caption)
.foregroundStyle(.secondary)
}
}
.navigationTitle("Publikované články")
}
}
Filtrování a řazení přímo v deklaraci property wrapperu. SwiftUI view se automaticky překreslí pokaždé, když se změní data odpovídající dotazu. Žádné manuální refreshe, žádné NotificationCenter. Prostě to funguje.
Dynamické dotazy
V praxi ale často potřebujete měnit parametry dotazu za běhu — třeba podle vyhledávacího textu nebo vybraného filtru. K tomu slouží inicializátor view s parametrickým @Query:
struct FilteredArticlesView: View {
@Query var articles: [Article]
init(searchText: String, showPublishedOnly: Bool) {
let predicate: Predicate
if searchText.isEmpty && !showPublishedOnly {
predicate = #Predicate { _ in true }
} else if searchText.isEmpty {
predicate = #Predicate { $0.isPublished }
} else if !showPublishedOnly {
predicate = #Predicate { article in
article.title.localizedStandardContains(searchText)
}
} else {
predicate = #Predicate { article in
article.isPublished &&
article.title.localizedStandardContains(searchText)
}
}
_articles = Query(
filter: predicate,
sort: [SortDescriptor(\.publishedAt, order: .reverse)]
)
}
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}
Predikáty: Síla a úskalí
Makro #Predicate je typově bezpečná náhrada za staré dobré NSPredicate. Kompilátor ověří, že přistupujete k existujícím vlastnostem správných typů, a chyby odhalí ještě před spuštěním aplikace. Pod kapotou se predikáty překládají na SQL dotazy, takže filtrování probíhá na úrovni databáze — ne v paměti.
Predikáty fungují i přes vztahy:
// Články od konkrétního autora
let authorName = "Jan Novák"
let predicate = #Predicate { article in
article.author?.name == authorName
}
// Články s konkrétním tagem
let tagName = "SwiftUI"
let predicate2 = #Predicate { article in
article.tags?.contains { tag in
tag.name == tagName
} == true
}
// Články publikované v posledním měsíci
let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: .now)!
let predicate3 = #Predicate { article in
article.publishedAt > oneMonthAgo && article.isPublished
}
Ale pozor — a tohle je důležité: ne všechno, co vypadá jako validní Swift kód, bude v predikátu skutečně fungovat. Některé operace projdou kompilací, ale za běhu vám aplikace spadne. Pár pravidel, která si vyplatí zapamatovat:
- Proměnné v predikátu musí být primitivních typů nebo
PersistentModel - Přístup přes keypath z externího stavu může způsobit pád — vždy si hodnotu uložte do lokální proměnné před predikátem
- Enumy musíte filtrovat přes jejich raw hodnotu, ne přímo přes enum case
- Od iOS 17.4 můžete predikáty kombinovat pomocí oficiálního API
FetchDescriptor: Pokročilé načítání mimo SwiftUI
Zatímco @Query funguje výhradně ve SwiftUI views, FetchDescriptor je univerzální nástroj pro načítání dat kdekoli v kódu. A je překvapivě flexibilní:
func loadRecentArticles(context: ModelContext) throws -> [Article] {
var descriptor = FetchDescriptor(
predicate: #Predicate { $0.isPublished },
sortBy: [SortDescriptor(\.publishedAt, order: .reverse)]
)
descriptor.fetchLimit = 20
descriptor.propertiesToFetch = [\.title, \.publishedAt, \.viewCount]
descriptor.relationshipKeyPathsForPrefetching = [\.author]
return try context.fetch(descriptor)
}
// Počet výsledků bez načítání objektů
func countPublishedArticles(context: ModelContext) throws -> Int {
let descriptor = FetchDescriptor(
predicate: #Predicate { $0.isPublished }
)
return try context.fetchCount(descriptor)
}
// Dávkové mazání
func deleteOldDrafts(context: ModelContext) throws {
let threeMonthsAgo = Calendar.current.date(byAdding: .month, value: -3, to: .now)!
try context.delete(
model: Article.self,
where: #Predicate { article in
!article.isPublished && article.publishedAt < threeMonthsAgo
}
)
}
Parametr propertiesToFetch je klíčový pro optimalizaci výkonu — načte pouze ty sloupce, které skutečně potřebujete. A relationshipKeyPathsForPrefetching předem načte související objekty, čímž elegantně eliminuje notoricky známý problém N+1 dotazů.
Migrace schémat s VersionedSchema
Každá reálná aplikace se vyvíjí a s ní se mění i datový model. SwiftData poskytuje robustní systém pro verzování schémat a migrace dat. A tady bych rád zdůraznil jednu věc, která vám ušetří spoustu bolestí hlavy:
Začněte s VersionedSchema od prvního dne.
Proč s tím začít hned
Přidání verzování schémat zpětně je výrazně složitější, než to nastavit od začátku. Pokud jste aplikaci vydali bez verzovaného schématu, SwiftData nemá žádnou referenční verzi, od které by mohl migrovat. Věřte mi, nechcete to řešit po vydání aplikace do App Store:
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Article.self, Author.self, Tag.self]
}
@Model
class Article {
var title: String
var content: String
var createdAt: Date
init(title: String, content: String, createdAt: Date = .now) {
self.title = title
self.content = content
self.createdAt = createdAt
}
}
@Model
class Author {
var name: String
var articles: [Article]?
init(name: String) {
self.name = name
}
}
@Model
class Tag {
var name: String
init(name: String) { self.name = name }
}
}
Přidání nové verze
Řekněme, že ve verzi 2.0 chcete přidat vlastnost isPublished k článku a přejmenovat createdAt na publishedAt. Takhle na to:
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Article.self, Author.self, Tag.self]
}
@Model
class Article {
var title: String
var content: String
@Attribute(originalName: "createdAt") var publishedAt: Date
var isPublished: Bool = false
var author: Author?
init(title: String, content: String, publishedAt: Date = .now, isPublished: Bool = false) {
self.title = title
self.content = content
self.publishedAt = publishedAt
self.isPublished = isPublished
}
}
// Author a Tag zůstávají stejné...
@Model
class Author {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Article.author)
var articles: [Article]?
init(name: String) { self.name = name }
}
@Model
class Tag {
var name: String
init(name: String) { self.name = name }
}
}
Migrační plán
S definovanými verzemi schémat vytvoříte migrační plán. SwiftData se o zbytek postará:
enum ArticleMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
// Lightweight migrace — přejmenování a přidání vlastnosti s výchozí hodnotou
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
)
}
Lightweight migrace zvládnou přidání, přejmenování a mazání vlastností, změny typů vztahů a další jednoduché operace. Ale co když potřebujete něco složitějšího?
Vlastní (custom) migrace
Představte si, že ve verzi 3.0 chcete sloučit duplicitní autory. To už lightweight migrace nezvládne a potřebujete vlastní migrační logiku:
static let migrateV2toV3 = MigrationStage.custom(
fromVersion: SchemaV2.self,
toVersion: SchemaV3.self,
willMigrate: { context in
// Před migrací: deduplikace autorů
let authors = try context.fetch(FetchDescriptor())
var seenNames: [String: SchemaV2.Author] = [:]
for author in authors {
if let existing = seenNames[author.name] {
// Přesun článků k existujícímu autorovi
for article in (author.articles ?? []) {
article.author = existing
}
context.delete(author)
} else {
seenNames[author.name] = author
}
}
try context.save()
},
didMigrate: { context in
// Po migraci: nastavení výchozích hodnot pro nové vlastnosti
let articles = try context.fetch(FetchDescriptor())
for article in articles {
if article.slug.isEmpty {
article.slug = article.title
.lowercased()
.replacingOccurrences(of: " ", with: "-")
}
}
try context.save()
}
)
Důležitý detail: willMigrate pracuje se starým schématem (V2), zatímco didMigrate už pracuje s novým schématem (V3). A co je skvělé — SwiftData automaticky řetězí migrace. Uživatel s V1 projde přes V2 do V3 bez ztráty dat.
Dědičnost modelů v iOS 26
Jednou z nejočekávanějších novinek WWDC 2025 bylo přidání dědičnosti modelů do SwiftData. Před iOS 26 jste nemohli mít jeden @Model dědící z jiného — museli jste buď duplikovat vlastnosti, nebo používat kompozici. Upřímně, bylo to dost frustrující. Naštěstí se to změnilo:
// Základní model — sdílené vlastnosti
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
var notes: String?
init(destination: String, startDate: Date, endDate: Date) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
// Podtřída pro služební cesty
@available(iOS 26, *)
@Model
class BusinessTrip: Trip {
var perDiem: Double = 0.0
var companyName: String = ""
var expenseReport: String?
}
// Podtřída pro osobní cesty
@available(iOS 26, *)
@Model
class PersonalTrip: Trip {
enum Reason: String, Codable {
case vacation
case family
case adventure
}
var reason: Reason = .vacation
}
Dědičnost přináší elegantní hierarchii modelů. Dotaz na Trip vrátí všechny cesty včetně služebních a osobních. Dotaz na BusinessTrip vrátí pouze služební cesty. Přesně tak, jak byste čekali.
Je tu ale jeden háček ohledně výkonu. SwiftData (respektive Core Data pod kapotou) neukládá podtřídy do oddělených tabulek. Všechna data jsou v jedné tabulce s rozlišovacím sloupcem. U velkých datových sad s mnoha podtřídami to může zpomalovat dotazy, protože databáze musí prohledávat celou tabulku.
Nezapomeňte také, že při přidání dědičnosti do existující aplikace potřebujete odpovídající migrační schéma. Nové podtřídy je nutné zaregistrovat v ModelContainer:
@available(iOS 26, *)
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Trip.self, BusinessTrip.self, PersonalTrip.self])
}
}
Synchronizace s CloudKit
SwiftData nabízí vestavěnou podporu synchronizace s iCloud přes CloudKit — a nastavení je překvapivě jednoduché. V mnoha případech stačí doslova pár řádků kódu.
Konfigurace projektu
V nastavení Signing & Capabilities vašeho targetu proveďte tyto kroky:
- Přidejte capability iCloud
- Zaškrtněte CloudKit
- Vytvořte nebo vyberte CloudKit kontejner
- Přidejte capability Background Modes
- Zaškrtněte Remote Notifications
Poté stačí v konfiguraci ModelContainer nastavit cloudKitDatabase:
let config = ModelConfiguration(
cloudKitDatabase: .automatic
)
let container = try ModelContainer(
for: Article.self, Author.self, Tag.self,
configurations: config
)
Pravidla pro CloudKit kompatibilitu
A teď ta méně příjemná část. CloudKit má specifické požadavky na strukturu modelů, a pokud je nedodržíte, synchronizace tiše selže. Žádná chybová hláška, žádný crash — prostě to přestane fungovat. Což je (upřímně řečeno) ta nejhorší kategorie bugů, jakou si dokážu představit. Takže si zapamatujte:
- Nepoužívejte
@Attribute(.unique)— CloudKit nepodporuje unikátní omezení na úrovni schématu - Všechny vlastnosti musí mít výchozí hodnoty nebo být volitelné (optional)
- Všechny vztahy musí být volitelné
- Pouze soukromá databáze — SwiftData nepodporuje sdílenou ani veřejnou CloudKit databázi
// Správně nakonfigurovaný model pro CloudKit
@Model
class Note {
var title: String = ""
var content: String = ""
var createdAt: Date = Date.now
var isPinned: Bool = false
@Relationship(deleteRule: .nullify)
var folder: Folder?
@Relationship(deleteRule: .cascade)
var attachments: [Attachment]? = []
init(title: String = "", content: String = "") {
self.title = title
self.content = content
}
}
Znovu zdůrazním ten klíčový detail: pokud váš model používá @Attribute(.unique) a zároveň CloudKit synchronizaci, synchronizace se tiše zastaví. Žádná chyba v konzoli, nic. Při ladění vždy kontrolujte CloudKit Dashboard v Apple Developer portálu — tam uvidíte, co se doopravdy děje.
Řešení problému s obnovením @Query
Známý problém, na který dřív nebo později narazíte: @Query se nemusí automaticky aktualizovat, když data dorazí přes CloudKit push notifikaci. Řešením je dynamický @Query s externím triggerem pro obnovení:
struct NotesListView: View {
@Query var notes: [Note]
@State private var refreshTrigger = UUID()
init(sortOrder: SortDescriptor = SortDescriptor(\.createdAt, order: .reverse)) {
_notes = Query(sort: [sortOrder])
}
var body: some View {
List(notes) { note in
NoteRowView(note: note)
}
.refreshable {
refreshTrigger = UUID()
}
.id(refreshTrigger)
}
}
Není to úplně nejelegantnější řešení, ale funguje spolehlivě.
Práce s ModelActor na pozadí
SwiftData je úzce svázaný s hlavním vláknem, což je skvělé pro UI aktualizace, ale problematické pro náročné operace jako import velkého množství dat. Makro @ModelActor řeší přesně tohle — vytvoří aktor s vlastním ModelContext pro práci na pozadí:
@ModelActor
actor DataImporter {
func importArticles(from jsonData: Data) throws -> Int {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let importedArticles = try decoder.decode([ArticleDTO].self, from: jsonData)
var count = 0
for dto in importedArticles {
let article = Article(
title: dto.title,
content: dto.content,
publishedAt: dto.publishedAt
)
modelContext.insert(article)
count += 1
// Periodické ukládání pro snížení paměťové náročnosti
if count % 100 == 0 {
try modelContext.save()
}
}
try modelContext.save()
return count
}
func cleanupOldArticles(olderThan date: Date) throws -> Int {
let predicate = #Predicate { article in
!article.isPublished && article.publishedAt < date
}
let descriptor = FetchDescriptor(predicate: predicate)
let oldArticles = try modelContext.fetch(descriptor)
for article in oldArticles {
modelContext.delete(article)
}
try modelContext.save()
return oldArticles.count
}
}
Použití v SwiftUI je přímočaré:
struct ImportView: View {
@Environment(\.modelContext) private var context
@State private var importCount = 0
@State private var isImporting = false
var body: some View {
VStack {
Button("Importovat články") {
isImporting = true
Task {
let importer = DataImporter(modelContainer: context.container)
let count = try await importer.importArticles(from: sampleData)
importCount = count
isImporting = false
}
}
.disabled(isImporting)
if isImporting {
ProgressView("Importuji...")
}
Text("Importováno: \(importCount) článků")
}
}
}
Důležité pravidlo: objekty vytvořené v jednom ModelContext nelze přímo používat v jiném. Pokud potřebujete předat objekt mezi kontexty, použijte persistentModelID a znovu ho načtěte v cílovém kontextu. Porušení tohohle pravidla vede k těžko reprodukovatelným crashům.
Testování SwiftData kódu
Pro unit testy je klíčové používat in-memory konfiguraci — testy neovlivní reálnou databázi a poběží rychle:
import Testing
import SwiftData
@testable import MyApp
struct ArticleTests {
var context: ModelContext
init() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: Article.self, Author.self, Tag.self,
configurations: config
)
context = ModelContext(container)
}
@Test func articleCreation() throws {
let article = Article(title: "Test", content: "Obsah testu")
context.insert(article)
try context.save()
let descriptor = FetchDescriptor()
let articles = try context.fetch(descriptor)
#expect(articles.count == 1)
#expect(articles.first?.title == "Test")
}
@Test func cascadeDelete() throws {
let author = Author(name: "Testovací autor")
let article = Article(title: "Test", content: "Obsah", author: author)
author.articles = [article]
context.insert(author)
try context.save()
context.delete(author)
try context.save()
let articles = try context.fetch(FetchDescriptor())
#expect(articles.isEmpty) // Cascade delete smazal i článek
}
@Test func predicateFiltering() throws {
let published = Article(title: "Publikovaný", content: "A")
published.isPublished = true
let draft = Article(title: "Koncept", content: "B")
draft.isPublished = false
context.insert(published)
context.insert(draft)
try context.save()
let predicate = #Predicate { $0.isPublished }
let descriptor = FetchDescriptor(predicate: predicate)
let results = try context.fetch(descriptor)
#expect(results.count == 1)
#expect(results.first?.title == "Publikovaný")
}
}
Osvědčené postupy a časté chyby
1. Vždy používejte VersionedSchema
Opakuji to potřetí, protože je to skutečně tak důležité: začněte s verzováním schémat od prvního commitu. Migrace dat bez referenční verze je noční můra, a řešit ji po vydání do App Store je ještě horší.
2. Pozor na výkon s velkými datovými sadami
SwiftData je obecně pomalejší než přímé Core Data operace. Pro velké datové sady doporučuji:
- Používejte
fetchLimitpro stránkování - Nastavte
propertiesToFetchpro načítání jen potřebných sloupců - Definujte indexy pomocí
#Indexpro často filtrované vlastnosti - Náročné operace přesuňte na pozadí přes
@ModelActor
3. Nepřistupujte k objektům napříč kontexty
Nikdy nepředávejte @Model objekt mezi různými ModelContext instancemi. Použijte persistentModelID:
// Špatně — crash nebo neočekávané chování
let article = try mainContext.fetch(descriptor).first!
backgroundContext.delete(article) // CHYBA!
// Správně — předání přes ID
let articleID = article.persistentModelID
let backgroundArticle = backgroundContext.model(for: articleID) as? Article
if let backgroundArticle {
backgroundContext.delete(backgroundArticle)
}
4. Zvažte dopad na CloudKit od začátku
Pokud plánujete CloudKit synchronizaci, navrhněte modely s ohledem na její omezení hned od začátku. Přidání CloudKit do existující aplikace s .unique atributy vyžaduje migraci schématu a odstranění unikátních omezení — a to může být docela bolestivá operace.
5. Nepoužívejte SwiftData tam, kde nestačí
SwiftData je skvělý pro většinu aplikací, ale má své limity. Momentálně nepodporuje:
- Agregační funkce (GROUP BY, SUM, AVG)
- Plnohodnotný ekvivalent NSFetchedResultsController pro sekcionované seznamy
- Sdílenou CloudKit databázi
- Složité dotazy, které by v čistém SQL byly triviální
Pro tyto scénáře může být lepší volbou Core Data, nebo dokonce přímý přístup k SQLite.
Praktický příklad: Správce poznámek od nuly
Teorie je fajn, ale nejlépe se učí na reálném příkladu. Pojďme si vytvořit jednoduchý správce poznámek, který demonstruje klíčové koncepty SwiftData v praxi.
Definice modelů
import SwiftData
import Foundation
@Model
class Notebook {
var name: String
var colorHex: String
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Note.notebook)
var notes: [Note]?
@Transient var noteCount: Int {
notes?.count ?? 0
}
init(name: String, colorHex: String = "#007AFF") {
self.name = name
self.colorHex = colorHex
self.createdAt = .now
}
}
@Model
class Note {
var title: String
var body: String
var createdAt: Date
var updatedAt: Date
var isFavorite: Bool
var notebook: Notebook?
init(title: String, body: String = "", notebook: Notebook? = nil) {
self.title = title
self.body = body
self.createdAt = .now
self.updatedAt = .now
self.isFavorite = false
self.notebook = notebook
}
}
Všimněte si použití @Transient u vypočítané vlastnosti noteCount. Tahle vlastnost se neukládá do databáze — existuje pouze v paměti a počítá se dynamicky ze vztahu. Zbytečně persitovat hodnotu, kterou můžete odvodit ze vztahu, je plýtvání.
Hlavní obrazovka s dynamickým filtrováním
struct NotesApp: App {
var body: some Scene {
WindowGroup {
NotebookListView()
}
.modelContainer(for: [Notebook.self, Note.self])
}
}
struct NotebookListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Notebook.createdAt) var notebooks: [Notebook]
@State private var showingNewNotebook = false
var body: some View {
NavigationStack {
List {
ForEach(notebooks) { notebook in
NavigationLink(value: notebook) {
HStack {
Circle()
.fill(Color(hex: notebook.colorHex))
.frame(width: 12, height: 12)
VStack(alignment: .leading) {
Text(notebook.name)
.font(.headline)
Text("\(notebook.noteCount) poznámek")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
.onDelete { indexSet in
for index in indexSet {
context.delete(notebooks[index])
}
}
}
.navigationTitle("Sešity")
.navigationDestination(for: Notebook.self) { notebook in
NoteListView(notebook: notebook)
}
.toolbar {
Button("Nový sešit") {
let notebook = Notebook(name: "Nový sešit")
context.insert(notebook)
}
}
}
}
}
struct NoteListView: View {
let notebook: Notebook
@Environment(\.modelContext) private var context
@Query var notes: [Note]
@State private var searchText = ""
init(notebook: Notebook) {
self.notebook = notebook
let notebookID = notebook.persistentModelID
_notes = Query(
filter: #Predicate { note in
note.notebook?.persistentModelID == notebookID
},
sort: [SortDescriptor(\.updatedAt, order: .reverse)]
)
}
var body: some View {
List(notes) { note in
VStack(alignment: .leading) {
HStack {
Text(note.title).font(.headline)
if note.isFavorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.font(.caption)
}
}
Text(note.body.prefix(100))
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
.swipeActions(edge: .leading) {
Button {
note.isFavorite.toggle()
note.updatedAt = .now
} label: {
Image(systemName: note.isFavorite ? "star.slash" : "star.fill")
}
.tint(.yellow)
}
}
.navigationTitle(notebook.name)
.searchable(text: $searchText, prompt: "Hledat poznámky")
.toolbar {
Button("Nová poznámka") {
let note = Note(title: "Bez názvu", notebook: notebook)
context.insert(note)
}
}
}
}
Tenhle příklad ukazuje hned několik klíčových vzorů najednou: filtrování přes vztah pomocí persistentModelID, dynamickou inicializaci @Query v konstruktoru view, swipe akce pro úpravu dat a kaskádové mazání. Při smazání sešitu se automaticky smažou i všechny poznámky — díky .cascade delete rule.
SwiftData vs. Core Data: Kdy co použít
S příchodem SwiftData se přirozeně nabízí otázka: mám migrovat stávající projekt z Core Data?
Odpověď závisí na vaší konkrétní situaci.
Použijte SwiftData pokud:
- Začínáte nový projekt a cílíte na iOS 17+
- Vaše datová vrstva je relativně jednoduchá — CRUD operace, základní dotazy, jednoduché vztahy
- Používáte SwiftUI jako primární UI framework
- Chcete rychlé prototypování s minimem boilerplate kódu
- Potřebujete jednoduchou iCloud synchronizaci soukromých dat
Zůstaňte u Core Data pokud:
- Potřebujete agregační funkce, skupinování nebo NSFetchedResultsController se sekcemi
- Vaše aplikace vyžaduje sdílenou nebo veřejnou CloudKit databázi
- Pracujete s extrémně velkými datovými sadami, kde záleží na každé milisekundě
- Potřebujete pokročilé funkce jako batch operace na úrovni SQL
- Máte rozsáhlou existující Core Data infrastrukturu, kde by migrace nepřinesla dostatečný přínos
Dobrá zpráva na závěr: oba frameworky mohou koexistovat v jedné aplikaci. Můžete postupně přesouvat funkčnost z Core Data na SwiftData, aniž byste museli dělat velkou migraci najednou. Core Data modely a SwiftData modely dokážou sdílet stejný persistent store, takže přechod může být opravdu plynulý.
Shrnutí
SwiftData za tři roky své existence urazil obrovský kus cesty. Z experimentálního frameworku s řadou nedostatků se stal seriózní nástroj pro persistenci dat. Přidání dědičnosti modelů v iOS 26 ukazuje, že Apple tuto technologii bere vážně a aktivně ji rozvíjí.
Pro nové projekty je SwiftData jasná volba — jeho integrace se SwiftUI je bezkonkurenční a psát v něm kód je radost. Pro existující projekty s Core Data doporučuji postupnou migraci komponentu po komponentě.
A ty nejdůležitější rady? Začněte s VersionedSchema od prvního dne, navrhněte modely s ohledem na CloudKit, používejte @ModelActor pro náročné operace a důkladně testujte migrace. S těmihle principy v hlavě bude vaše práce se SwiftData hladká a bezproblémová.