How do SwiftData’s @Model, ModelContainer, ModelContext, #Predicate and FetchDescriptor work together?
Predict first
Three items are inserted in a context and saved. What do the unsaved check, a predicate fetch, a paged fetch and a count return?
import SwiftData
import Foundation
@Model final class Item {
var title: String
var done: Bool
init(title: String, done: Bool = false) { self.title = title; self.done = done }
}
let container = try ModelContainer(for: Item.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
let ctx = ModelContext(container)
ctx.insert(Item(title: "a")); ctx.insert(Item(title: "b", done: true)); ctx.insert(Item(title: "c"))
print(ctx.hasChanges)
try ctx.save()
let open = try ctx.fetch(FetchDescriptor<Item>(predicate: #Predicate { $0.done == false }, sortBy: [SortDescriptor(\.title, order: .reverse)]))
print(open.map(\.title))
var page = FetchDescriptor<Item>(sortBy: [SortDescriptor(\.title)])
page.fetchLimit = 2
page.fetchOffset = 1
print(try ctx.fetch(page).map(\.title))
print(try ctx.fetchCount(FetchDescriptor<Item>(predicate: #Predicate { $0.done })))
- true, ["c", "a"], ["b", "c"], 1
- false, ["a", "c"], ["a", "b"], 1
- true, ["c", "a"], ["b", "c"], 2
Mental model. The container is the filing cabinet, the context is your desk where you edit papers, and a predicate is the instruction to the clerk: “bring me only the open ones, newest first”.
A class marked @Model becomes persistent: the macro rewrites its stored properties so changes are tracked and observable (it also makes the class Observable, so SwiftUI updates without extra wrappers), and conforms it to PersistentModel. Properties can be basic types, Codable values and other models (relationships, with delete rules via @Relationship); use @Attribute(.unique) for uniqueness (which upserts on conflict) and @Transient for non-persisted properties. A ModelContainer owns the schema and store (on disk, or in memory with ModelConfiguration(isStoredInMemoryOnly: true) for tests and previews). A ModelContext is the working area, like Core Data’s context: you insert and delete models, and save() persists. In SwiftUI inject the container with .modelContainer(for:) and read the context with @Environment(\.modelContext); the mainContext has autosave enabled, so pending changes are saved at convenient moments (not instantly, and not guaranteed before the process is killed), so call save() explicitly when durability at a moment matters.
Query with a FetchDescriptor<Model>: a #Predicate macro (type-checked Swift expressions that are translated into SQL, such as $0.done == false), sortBy: [SortDescriptor(\.title)], fetchLimit and fetchOffset for paging, and fetchCount for counts without loading models. @Query does the same inside SwiftUI views and refreshes the view when data changes. Limits to know: #Predicate supports a subset of Swift (no arbitrary function calls), so unsupported expressions fail to compile or at runtime; and the framework is newer than Core Data, so check feature support for your deployment target (history tracking, batch operations, custom stores vary by OS version). The same model rules apply as in any ORM: keep models as the persistence representation and map to domain types at a boundary if the domain should not depend on SwiftData, and avoid fetching everything just to filter in memory.
There are unsaved inserts. Open items sorted descending are c, a. Offset 1 with limit 2 skips "a". Only one item is done.
Common traps
- Relying on autosave to persist before a crash or kill. It happens at the framework’s discretion; call
save()when it matters. - Fetching everything and filtering in Swift. Use a
#Predicateand limits so the filtering happens in SQLite.
They might follow up with
- How do you test with SwiftData? An in-memory
ModelConfigurationfor a fresh store per test. - What does
@Queryadd? A fetch bound to a SwiftUI view that updates when the data changes.
Related questions
- 13.4.2 How do ModelActor, context-per-actor and VersionedSchema work in SwiftData, and what can you pass between…
- 13.3.1 What are the parts of the Core Data stack, what is a managed object context, and what is a fault?
- 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
How do ModelActor, context-per-actor and VersionedSchema work in… →