Which parts of a SwiftData context’s lifecycle need care: autosave timing, explicit saves, rollback, and what a predicate can express?
Predict first
What do autosave, a predicate fetch and a rollback show?
import Foundation
import SwiftData
@Model final class Item {
var name: String
var qty: Int
init(name: String, qty: Int) { self.name = name; self.qty = qty }
}
@MainActor func run() throws {
let container = try ModelContainer(for: Item.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
let ctx = container.mainContext
for (n, q) in [("apple", 3), ("pear", 0), ("fig", 12)] { ctx.insert(Item(name: n, qty: q)) }
print("autosave:", ctx.autosaveEnabled, "| hasChanges before save:", ctx.hasChanges)
try ctx.save()
let p = #Predicate<Item> { $0.qty > 0 && $0.name.contains("p") }
let found = try ctx.fetch(FetchDescriptor<Item>(predicate: p, sortBy: [SortDescriptor(\.name)]))
print("fetched:", found.map(\.name))
var d = FetchDescriptor<Item>(sortBy: [SortDescriptor(\.qty, order: .reverse)])
d.fetchLimit = 2
print("top 2 by qty:", try ctx.fetch(d).map(\.name))
found[0].qty = 99
print("hasChanges after an edit:", ctx.hasChanges)
ctx.rollback()
print("after rollback qty:", found[0].qty, "| hasChanges:", ctx.hasChanges)
print("count:", try ctx.fetchCount(FetchDescriptor<Item>()))
}
try MainActor.assumeIsolated { try run() }
- autosave: true | hasChanges before save: true / fetched: ["apple"] / top 2 by qty: ["fig", "apple"] / hasChanges after an edit: true / after rollback qty: 3 | hasChanges: false / count: 3
- autosave: false
- fetched: ["apple", "pear"]
Mental model. The context is a draft folder: edits sit in it, autosave files them away every so often, rollback tears up the draft, and a fetch descriptor is a precise request to the archive.
A ModelContext is SwiftData’s scratch pad, like a Core Data context. The main context (container.mainContext, also injected by .modelContainer) has autosaveEnabled == true (the check prints it): changes are saved automatically at sensible moments (a run-loop turn, app lifecycle events), so you often do not call save(), but you should call it explicitly before work that must be durable (before a sync, a share, or app termination paths you control), and any context you create yourself should be saved explicitly. hasChanges tells you whether there is something pending; rollback() discards unsaved changes, restoring the object’s qty from 99 to 3 in the check.
Fetching is done with a FetchDescriptor: a #Predicate (a macro that builds a typed expression tree: $0.qty > 0 && $0.name.contains("p")), sortBy descriptors, a fetchLimit, an fetchOffset and propertiesToFetch; fetchCount counts without loading. Know the limits: predicates support only a defined set of operations (comparisons, string contains and starts(with:), collection filters and arithmetic; arbitrary Swift methods and your own computed properties are not allowed and fail to compile or at run time on some OS versions), they are translated to SQL so they run in the store, and sort keys must be stored properties. Outside SwiftUI, use a ModelActor for background work and pass PersistentIdentifiers, not model objects, between contexts (Q336). Expect behaviour to vary by OS release (SwiftData is young): test on your minimum target, keep a repository boundary around it (Q337), and plan schema versions early.
The main context autosaves; the predicate keeps only matching items; rollback restores saved values.
Common traps
- Relying on autosave for data that must survive an immediate termination. Save explicitly at the moments that matter.
- Putting arbitrary Swift code in a
#Predicate. Only supported operations translate to the store; others fail.
They might follow up with
- How do I do work off the main thread? A
ModelActorwith its own context; return identifiers, not models. - How is this different from Core Data’s fault behaviour? The concepts are the same (identity, contexts, lazy loading) behind a macro-based API.
Related questions
- 13.10.3 What do delete rules do, and what happens when you set an attribute to a value of the wrong type?
- 13.10.1 Two contexts change the same object and both save. What does each merge policy do, and what is the default?
- 13.4.1 How do SwiftData’s @Model, ModelContainer, ModelContext, #Predicate and FetchDescriptor work together?
- 13.4.2 How do ModelActor, context-per-actor and VersionedSchema work in SwiftData, and what can you pass between…
- 13.5.1 What is the repository pattern, and why map storage models to domain models?
← What do delete rules do, and what happens when you set an attribute…