swiftdata-persistence
SwiftData Persistence
Comprehensive guide to SwiftData framework, the @Model macro, reactive queries, relationships, and native iCloud synchronization for iOS 26 development.
Prerequisites
- iOS 17+ for SwiftData (iOS 26 recommended)
- Xcode 26+
@Model Macro Basics
Defining a Model
import SwiftData
@Model
class Note {
var title: String
var content: String
var createdAt: Date
var isPinned: Bool
init(title: String, content: String = "") {
self.title = title
self.content = content
self.createdAt = Date()
self.isPinned = false
}
}
What @Model Provides
The @Model macro automatically:
- Makes the class persistable
- Tracks property changes
- Enables SwiftUI observation
- Generates schema metadata
Model Requirements
@Model
class Item {
// All stored properties must be:
// - Codable types (String, Int, Date, Data, etc.)
// - Other @Model types (relationships)
// - Arrays/optionals of the above
var name: String // ✓ Codable
var count: Int // ✓ Codable
var timestamp: Date // ✓ Codable
var data: Data // ✓ Codable
var tags: [String] // ✓ Array of Codable
var metadata: [String: String] // ✓ Dictionary of Codable
var related: RelatedItem? // ✓ Optional @Model relationship
// Computed properties are NOT persisted
var displayName: String {
name.uppercased()
}
init(name: String) {
self.name = name
self.count = 0
self.timestamp = Date()
self.data = Data()
self.tags = []
}
}
Model Attributes
@Attribute Macro
@Model
class User {
// Unique constraint (NOT compatible with iCloud sync)
@Attribute(.unique)
var email: String
// Spotlight indexing
@Attribute(.spotlight)
var name: String
// External storage for large data
@Attribute(.externalStorage)
var profileImage: Data?
// Encryption (device-only, not synced to iCloud)
@Attribute(.encrypt)
var sensitiveData: String?
// Preserve value when nil assigned
@Attribute(.preserveValueOnDeletion)
var archiveReason: String?
// Ephemeral (not persisted)
@Attribute(.ephemeral)
var temporaryState: String?
// Custom original name for migration
@Attribute(originalName: "userName")
var displayName: String
init(email: String, name: String) {
self.email = email
self.name = name
self.displayName = name
}
}
@Transient Macro
@Model
class Document {
var title: String
var content: String
// Not persisted, recalculated
@Transient
var wordCount: Int = 0
init(title: String, content: String) {
self.title = title
self.content = content
self.wordCount = content.split(separator: " ").count
}
}
Relationships
One-to-Many Relationship
@Model
class Folder {
var name: String
// One folder has many notes
@Relationship(deleteRule: .cascade)
var notes: [Note] = []
init(name: String) {
self.name = name
}
}
@Model
class Note {
var title: String
var content: String
// Many notes belong to one folder (inverse)
var folder: Folder?
init(title: String, content: String = "", folder: Folder? = nil) {
self.title = title
self.content = content
self.folder = folder
}
}
Many-to-Many Relationship
@Model
class Note {
var title: String
// Note can have many tags
@Relationship(inverse: \Tag.notes)
var tags: [Tag] = []
init(title: String) {
self.title = title
}
}
@Model
class Tag {
var name: String
// Tag can be on many notes
var notes: [Note] = []
init(name: String) {
self.name = name
}
}
Delete Rules
@Relationship(deleteRule: .cascade) // Delete related objects
@Relationship(deleteRule: .nullify) // Set relationship to nil (default)
@Relationship(deleteRule: .deny) // Prevent deletion if related exist
@Relationship(deleteRule: .noAction) // Do nothing
iCloud-Compatible Relationships
Important: For iCloud sync, all relationships MUST be optional:
@Model
class Note {
var title: String
// REQUIRED for iCloud: Optional relationships
var folder: Folder?
var tags: [Tag]? // Optional array
init(title: String) {
self.title = title
}
}
ModelContainer Configuration
Basic Setup
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Note.self, Folder.self, Tag.self])
}
}
Custom Configuration
@main
struct MyApp: App {
let container: ModelContainer
init() {
let schema = Schema([Note.self, Folder.self, Tag.self])
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
do {
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Failed to configure SwiftData: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Multiple Configurations
let userConfig = ModelConfiguration(
"UserData",
schema: Schema([User.self]),
url: userDataURL
)
let cacheConfig = ModelConfiguration(
"Cache",
schema: Schema([CachedItem.self]),
isStoredInMemoryOnly: true
)
let container = try ModelContainer(
for: Schema([User.self, CachedItem.self]),
configurations: userConfig, cacheConfig
)
Native iCloud Sync
Enabling iCloud Sync (One Line!)
SwiftData includes native iCloud sync - no CloudKit code required:
@main
struct MyApp: App {
let container: ModelContainer
init() {
let schema = Schema([Note.self, Tag.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .automatic // That's it!
)
do {
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Failed to configure SwiftData: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
cloudKitDatabase Options
// Automatic iCloud sync (recommended)
cloudKitDatabase: .automatic
// Specific CloudKit container
cloudKitDatabase: .private("iCloud.com.yourcompany.yourapp")
// No iCloud sync (local only)
cloudKitDatabase: .none
Xcode Setup for iCloud
- Select your target in Xcode
- Go to "Signing & Capabilities"
- Click "+ Capability"
- Add "iCloud"
- Check "CloudKit"
- Select or create a CloudKit container
- Add "Background Modes" capability
- Check "Remote notifications"
iCloud-Compatible Model Requirements
Critical rules for iCloud sync:
@Model
class Note {
// ✓ Default values for non-optional properties
var title: String = ""
var content: String = ""
var createdAt: Date = Date()
// ✓ Optional relationships
var folder: Folder?
var tags: [Tag]?
// ✗ NO unique constraints (not supported by CloudKit)
// @Attribute(.unique) var id: String // DON'T DO THIS
// ✗ NO deny delete rules
// @Relationship(deleteRule: .deny) // DON'T DO THIS
init(title: String = "", content: String = "") {
self.title = title
self.content = content
self.createdAt = Date()
}
}
Schema Migration for iCloud
After shipping to production:
// DO:
// - Add new optional properties with defaults
// - Add new optional relationships
// DON'T:
// - Delete properties (data loss)
// - Rename properties (treated as delete + add)
// - Change property types
// - Add required properties without defaults
Initialize CloudKit Schema
Before first production release:
#if DEBUG
// Run once to create CloudKit schema
try container.mainContext.initializeCloudKitSchema()
#endif
@Query Macro
Basic Query
struct NotesListView: View {
@Query var notes: [Note]
var body: some View {
List(notes) { note in
Text(note.title)
}
}
}
Sorted Query
// Single sort
@Query(sort: \Note.createdAt, order: .reverse)
var notes: [Note]
// Multiple sorts
@Query(sort: [
SortDescriptor(\Note.isPinned, order: .reverse),
SortDescriptor(\Note.createdAt, order: .reverse)
])
var notes: [Note]
Filtered Query
// Static predicate
@Query(filter: #Predicate<Note> { note in
note.isPinned == true
})
var pinnedNotes: [Note]
// Complex predicate
@Query(filter: #Predicate<Note> { note in
note.title.contains("Swift") && !note.content.isEmpty
})
var swiftNotes: [Note]
Dynamic Filtering
struct SearchableNotesView: View {
@State private var searchText = ""
var body: some View {
FilteredNotesView(searchText: searchText)
.searchable(text: $searchText)
}
}
struct FilteredNotesView: View {
@Query var notes: [Note]
init(searchText: String) {
let predicate = #Predicate<Note> { note in
searchText.isEmpty || note.title.localizedStandardContains(searchText)
}
_notes = Query(filter: predicate, sort: \.createdAt, order: .reverse)
}
var body: some View {
List(notes) { note in
Text(note.title)
}
}
}
Query with Limit
@Query(sort: \Note.createdAt, order: .reverse)
var recentNotes: [Note]
// In view, limit manually
List(recentNotes.prefix(10)) { note in
Text(note.title)
}
Query Animations
@Query(sort: \Note.title, animation: .default)
var notes: [Note]
ModelContext Operations
Accessing Context
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// ...
}
Creating Objects
func createNote() {
let note = Note(title: "New Note")
modelContext.insert(note)
// Auto-saved on SwiftUI lifecycle events
}
Explicit Save
func saveChanges() {
do {
try modelContext.save()
} catch {
print("Save failed: \(error)")
}
}
Deleting Objects
func deleteNote(_ note: Note) {
modelContext.delete(note)
}
func deleteNotes(at offsets: IndexSet) {
for index in offsets {
modelContext.delete(notes[index])
}
}
Fetching with Descriptor
func fetchRecentNotes() throws -> [Note] {
let descriptor = FetchDescriptor<Note>(
predicate: #Predicate { $0.isPinned },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return try modelContext.fetch(descriptor)
}
// With limit
func fetchTopNotes(limit: Int) throws -> [Note] {
var descriptor = FetchDescriptor<Note>(
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
descriptor.fetchLimit = limit
return try modelContext.fetch(descriptor)
}
Batch Operations
// Delete all matching predicate
try modelContext.delete(model: Note.self, where: #Predicate { note in
note.createdAt < cutoffDate
})
// Enumerate for batch processing
let descriptor = FetchDescriptor<Note>()
try modelContext.enumerate(descriptor) { note in
note.processedAt = Date()
}
#Predicate Macro
Basic Predicates
// Equality
#Predicate<Note> { $0.isPinned == true }
// Comparison
#Predicate<Note> { $0.createdAt > someDate }
// String contains
#Predicate<Note> { $0.title.contains("Swift") }
// Case-insensitive contains
#Predicate<Note> { $0.title.localizedStandardContains(searchText) }
Compound Predicates
// AND
#Predicate<Note> { note in
note.isPinned && note.title.contains("Important")
}
// OR
#Predicate<Note> { note in
note.isPinned || note.folder?.name == "Favorites"
}
// NOT
#Predicate<Note> { note in
!note.content.isEmpty
}
Optional Handling
#Predicate<Note> { note in
note.folder?.name == "Work"
}
// Check for nil
#Predicate<Note> { note in
note.folder != nil
}
Array Predicates
// Array contains
#Predicate<Note> { note in
note.tags?.contains(where: { $0.name == "Important" }) ?? false
}
// Array is empty
#Predicate<Note> { note in
note.tags?.isEmpty ?? true
}
Model Inheritance (iOS 26)
Base and Derived Models
@Model
class MediaItem {
var title: String
var createdAt: Date
init(title: String) {
self.title = title
self.createdAt = Date()
}
}
@Model
final class Photo: MediaItem {
var imageData: Data?
var resolution: String?
init(title: String, imageData: Data?) {
super.init(title: title)
self.imageData = imageData
}
}
@Model
final class Video: MediaItem {
var duration: TimeInterval
var thumbnailData: Data?
init(title: String, duration: TimeInterval) {
super.init(title: title)
self.duration = duration
}
}
Polymorphic Queries
// Query all media items (photos and videos)
@Query var allMedia: [MediaItem]
// Query only photos
@Query var photos: [Photo]
Background Operations
Background Context
func importData() async {
let container = modelContainer
await Task.detached {
let context = ModelContext(container)
// Perform operations
for item in largeDataSet {
let note = Note(title: item.title)
context.insert(note)
}
try? context.save()
}.value
}
Actor Isolation
@ModelActor
actor DataImporter {
func importNotes(from data: [ImportData]) throws {
for item in data {
let note = Note(title: item.title, content: item.content)
modelContext.insert(note)
}
try modelContext.save()
}
}
// Usage
let importer = DataImporter(modelContainer: container)
try await importer.importNotes(from: importData)
Migration
Lightweight Migration (Automatic)
SwiftData handles lightweight migrations automatically:
- Adding new properties with defaults
- Removing properties
- Adding optional relationships
Custom Migration
enum MySchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self]
}
@Model
class Note {
var title: String
var content: String
init(title: String, content: String) {
self.title = title
self.content = content
}
}
}
enum MySchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Note.self]
}
@Model
class Note {
var title: String
var content: String
var createdAt: Date // New property
init(title: String, content: String) {
self.title = title
self.content = content
self.createdAt = Date()
}
}
}
enum MyMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[MySchemaV1.self, MySchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: MySchemaV1.self,
toVersion: MySchemaV2.self
)
}
// Use in container
let container = try ModelContainer(
for: Note.self,
migrationPlan: MyMigrationPlan.self
)
Testing
In-Memory Testing
import Testing
import SwiftData
@Test
func testNoteCreation() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Note.self, configurations: config)
let context = ModelContext(container)
let note = Note(title: "Test", content: "Content")
context.insert(note)
let descriptor = FetchDescriptor<Note>()
let notes = try context.fetch(descriptor)
#expect(notes.count == 1)
#expect(notes.first?.title == "Test")
}
SwiftUI Preview with Sample Data
@MainActor
let previewContainer: ModelContainer = {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Note.self, configurations: config)
// Insert sample data
let context = container.mainContext
let sampleNotes = [
Note(title: "First Note", content: "Content 1"),
Note(title: "Second Note", content: "Content 2")
]
sampleNotes.forEach { context.insert($0) }
return container
}()
#Preview {
NotesListView()
.modelContainer(previewContainer)
}
Best Practices
1. Design for iCloud from the Start
// GOOD: iCloud-compatible model
@Model
class Note {
var title: String = ""
var content: String = ""
var folder: Folder? // Optional relationship
var tags: [Tag]? // Optional array
init(title: String = "") {
self.title = title
}
}
// AVOID: iCloud-incompatible
@Model
class Note {
@Attribute(.unique) var id: String // Not supported
var folder: Folder // Non-optional relationship
}
2. Use @Query for Reactive Data
// GOOD: Reactive updates
@Query(sort: \Note.createdAt)
var notes: [Note]
// AVOID: Manual fetching in views
@State private var notes: [Note] = []
func loadNotes() {
notes = try? context.fetch(FetchDescriptor<Note>())
}
3. Explicit Save for Critical Data
func saveImportantChange() {
modelContext.insert(criticalData)
do {
try modelContext.save()
} catch {
// Handle error appropriately
}
}
4. Use Background Contexts for Heavy Work
func importLargeDataset() async {
await Task.detached {
let context = ModelContext(container)
// Heavy operations
try? context.save()
}.value
}
Official Resources
More from bluewaves-creations/bluewaves-skills
photographer-testino
Generate images in Mario Testino's glamorous vibrant style. Use when users ask for Testino style, high fashion glamour, bold saturated colors, warm luxurious photography, dynamic sensual energy.
35photographer-lindbergh
Generate images in Peter Lindbergh's iconic black and white style. Use when users ask for Lindbergh style, raw authentic beauty, emotional B&W portraits, supermodel aesthetic, or unretouched natural photography.
30photographer-lachapelle
Generate images in David LaChapelle's surreal pop art style. Use when users ask for LaChapelle style, pop surrealism, hyper-saturated colors, theatrical staging, baroque maximalism, kitsch aesthetic.
24epub-creator
Create production-quality EPUB 3 ebooks from markdown and images with automated QA, formatting fixes, and validation. Use when creating ebooks, converting markdown to EPUB, or compiling chapters into a publishable book. Handles markdown quirks, generates TOC, adds covers, and validates output automatically.
22photographer-vonunwerth
Generate images in Ellen von Unwerth's playful vintage style. Use when users ask for von Unwerth style, playful sensuality, vintage film noir, whimsical feminine photography, retro glamour, narrative storytelling.
19photographer-ritts
Generate images in Herb Ritts' sculptural black and white style. Use when users ask for Ritts style, classical Greek aesthetic, sculptural body photography, California golden hour, minimalist athletic portraits.
18