```html SwiftUI: How to Use SwiftData (iOS 17+, 2026)

How to Use SwiftData in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: SwiftData, @Model Updated: May 11, 2026
TL;DR

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

  1. @Model generates persistence + observation. Attaching the @Model macro to a Swift class automatically synthesises Observable conformance (iOS 17's new Observation framework) and hooks the class into SwiftData's SQLite back-end. Mutating any stored property — like item.isDone.toggle() — is observed and persisted without an explicit save call.
  2. .modelContainer(for:) bootstraps the stack. Calling .modelContainer(for: TodoItem.self) on the WindowGroup creates a ModelContainer and injects a live ModelContext into the SwiftUI environment for all descendants.
  3. @Query drives reactive list updates. @Query(sort: \TodoItem.createdAt, order: .reverse) fetches all TodoItem records sorted newest-first and re-renders the List automatically whenever the store changes — no NSFetchedResultsController delegate needed.
  4. context.insert / context.delete for mutations. context.insert(item) in addItem() registers the new object with the store. context.delete(items[index]) in deleteItems schedules the record for deletion. SwiftData auto-saves on the next run-loop turn.
  5. In-memory container for previews and tests. ModelConfiguration(isStoredInMemoryOnly: true) in #Preview gives 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

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?
No. SwiftData is iOS 17+ only and cannot be back-deployed. The framework relies on the new 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?
Define versioned schemas using 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?
SwiftData is built on top of Core Data, which works in both UIKit and AppKit. You can use 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.

```