How to Use SwiftData in SwiftUI
Annotate a Swift class with @Model to make it persistable, attach a ModelContainer at the app root, then read with @Query and write via modelContext in any view.
import SwiftData
@Model
class TodoItem {
var title: String
var isDone: Bool
var createdAt: Date
init(title: String) {
self.title = title
self.isDone = false
self.createdAt = .now
}
}
// In any SwiftUI view:
@Query(sort: \TodoItem.createdAt) var items: [TodoItem]
@Environment(\.modelContext) private var context
Full implementation
The example below wires together a complete task-list app using SwiftData end-to-end. The @Model macro on TodoItem generates all the observation and persistence machinery you previously wrote by hand with Core Data. .modelContainer(for:) on the WindowGroup creates a shared store that every child view can access through the environment.
import SwiftUI
import SwiftData
// ── Model ─────────────────────────────────────────────────────────────────
@Model
class TodoItem {
var title: String
var isDone: Bool
var createdAt: Date
init(title: String) {
self.title = title
self.isDone = false
self.createdAt = .now
}
}
// ── App entry point ────────────────────────────────────────────────────────
@main
struct TodoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: TodoItem.self) // boots the SQLite store
}
}
// ── Content view ───────────────────────────────────────────────────────────
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query(sort: \TodoItem.createdAt, order: .reverse) private var items: [TodoItem]
@State private var newTitle = ""
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
HStack {
Image(systemName: item.isDone ? "checkmark.circle.fill" : "circle")
.foregroundStyle(item.isDone ? .green : .secondary)
.onTapGesture { item.isDone.toggle() } // auto-saves via @Model
Text(item.title)
.strikethrough(item.isDone)
.foregroundStyle(item.isDone ? .secondary : .primary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(item.title), \(item.isDone ? "done" : "pending")")
.accessibilityHint("Double-tap to toggle")
}
.onDelete(perform: deleteItems)
}
.navigationTitle("Todos")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("New item…", text: $newTitle)
.textFieldStyle(.roundedBorder)
.onSubmit { addItem() }
Button("Add", action: addItem)
.disabled(newTitle.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding(.horizontal)
}
}
}
}
private func addItem() {
let trimmed = newTitle.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
let item = TodoItem(title: trimmed)
context.insert(item) // inserts into the persistent store
newTitle = ""
}
private func deleteItems(at offsets: IndexSet) {
for index in offsets {
context.delete(items[index])
}
}
}
// ── Preview ────────────────────────────────────────────────────────────────
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: TodoItem.self, configurations: config)
let sample = TodoItem(title: "Buy oat milk")
sample.isDone = true
let sample2 = TodoItem(title: "Ship the app")
container.mainContext.insert(sample)
container.mainContext.insert(sample2)
return ContentView()
.modelContainer(container)
}
How it works
-
@Model generates persistence + observation. Attaching the
@Modelmacro to a Swift class automatically synthesisesObservableconformance (iOS 17's new Observation framework) and hooks the class into SwiftData's SQLite back-end. Mutating any stored property — likeitem.isDone.toggle()— is observed and persisted without an explicit save call. -
.modelContainer(for:) bootstraps the stack. Calling
.modelContainer(for: TodoItem.self)on theWindowGroupcreates aModelContainerand injects a liveModelContextinto the SwiftUI environment for all descendants. -
@Query drives reactive list updates.
@Query(sort: \TodoItem.createdAt, order: .reverse)fetches allTodoItemrecords sorted newest-first and re-renders theListautomatically whenever the store changes — noNSFetchedResultsControllerdelegate needed. -
context.insert / context.delete for mutations.
context.insert(item)inaddItem()registers the new object with the store.context.delete(items[index])indeleteItemsschedules the record for deletion. SwiftData auto-saves on the next run-loop turn. -
In-memory container for previews and tests.
ModelConfiguration(isStoredInMemoryOnly: true)in#Previewgives Xcode a throwaway store that never touches disk, making previews fast and isolated.
Variants
Filtered queries with a predicate
Use @Query's filter: parameter to show only a subset of records. SwiftData compiles Swift key-path predicates directly to SQL.
// Show only incomplete items, sorted by title
@Query(
filter: #Predicate<TodoItem> { !$0.isDone },
sort: \TodoItem.title
)
private var pendingItems: [TodoItem]
// Pass a dynamic predicate from a parent view:
struct FilteredList: View {
let showDone: Bool
var query: Query<TodoItem, [TodoItem]> {
Query(filter: #Predicate { item in
showDone ? item.isDone : !item.isDone
}, sort: \TodoItem.createdAt)
}
var body: some View {
// Use query.wrappedValue to access results
List(query.wrappedValue) { item in
Text(item.title)
}
}
}
One-to-many relationships
SwiftData supports typed relationships with @Relationship. Mark a property on the parent model and SwiftData manages the join table automatically:
@Model
class Project {
var name: String
@Relationship(deleteRule: .cascade)
var todos: [TodoItem] = []
init(name: String) { self.name = name }
}
@Model
class TodoItem {
var title: String
var isDone: Bool
var project: Project? // inverse back-reference
// ... rest of init
}
Common pitfalls
- 🚫 SwiftData requires iOS 17+ exclusively. There is no back-deployment path to iOS 16. If your deployment target is iOS 16, you must use Core Data or a third-party store. SwiftData and Core Data can coexist in the same binary if you need to migrate incrementally.
-
🚫 Never pass a @Model object across actor boundaries unsafely. SwiftData model objects are not
Sendableby default. Background work should use a separateModelContextcreated from your container (container.newBackgroundContext()) and pass only the data you need (e.g. aPersistentIdentifier) back to the main actor. -
🚫 Avoid referencing @Query results in initializers.
@Queryis a property wrapper that's only valid inside aViewbody. Trying to readitemsbefore the view has appeared or inside a non-view type will crash at runtime. If you need data at init time, use aModelContextand perform a manualFetchDescriptorfetch. -
🚫 Schema migrations must be declared explicitly. Silently changing a
@Modelclass (adding/removing properties) between app versions without providing aSchemaMigrationPlanwill throw a fatal error on upgrade. Define a versioned schema and lightweight or custom migration stage before shipping schema changes. -
🚫 isStoredInMemoryOnly must be set per-configuration, not per-container. Forgetting to pass the in-memory configuration to your
ModelContainerin tests results in tests writing to your real app's database — a classic testing footgun that's easy to overlook when copy-pasting preview code.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement SwiftData persistence in SwiftUI for iOS 17+. Use SwiftData @Model, @Query, modelContext, and ModelContainer. Define at least one @Model class with a one-to-many @Relationship. Include a filtered @Query with a #Predicate. Make it accessible (VoiceOver labels on list rows). Add a #Preview with an in-memory ModelContainer and realistic sample data.
In Soarias's Build phase, paste this prompt directly into the implementation chat after locking your screen designs — Claude Code will wire the persistence layer to your existing SwiftUI views without overwriting your UI code.
Related
FAQ
Does SwiftData work on iOS 16?
Observation framework (also iOS 17+) and new Swift macro infrastructure introduced in Swift 5.9. If you need iOS 16 support, continue using Core Data with @FetchRequest and NSManagedObject, or use a third-party solution like GRDB. You can have both Core Data and SwiftData in the same project if your minimum deployment target is iOS 16 and you want SwiftData only on iOS 17+ devices — guard on if #available(iOS 17, *).
How do I migrate my schema when I add a new property?
VersionedSchema and declare a SchemaMigrationPlan with either a lightweight migration stage (MigrationStage.lightweight — works when you only add optional properties or add defaults) or a custom migration stage (MigrationStage.custom) for more complex changes. Pass the plan to ModelContainer(for:migrationPlan:) at app startup. Skipping this step when changing stored properties causes a runtime crash on devices with existing data.
What's the UIKit / AppKit equivalent of SwiftData?
NSPersistentContainer, NSManagedObjectContext, and NSFetchedResultsController in any UIKit app. SwiftData itself is framework-agnostic — its model layer doesn't depend on SwiftUI. You can create a ModelContainer and perform fetches using FetchDescriptor in a UIKit view controller, though you lose the declarative @Query property wrapper that makes it especially ergonomic in SwiftUI.
Last reviewed: 2026-05-11 by the Soarias team.