ModelState Usage
June 11, 2026 Β· View on GitHub
π ModelState lets you manage SwiftData @Model objects through AppState's dependency-injection model. Register a shared ModelContainer once; read and write models from anywhere β view models, services, or other non-view code β without threading ModelContext through your call stack.
π
ModelStaterequires Apple platforms with SwiftData support (iOS 17+, macOS 14+, tvOS 17+, watchOS 10+, visionOS 1+). These APIs are compiled out on Linux and Windows.
End-to-End Example
import AppState
import SwiftData
import SwiftUI
// 1. Define the model.
@Model
final class TodoItem {
var title: String
var isComplete: Bool
init(title: String, isComplete: Bool = false) {
self.title = title
self.isComplete = isComplete
}
}
// 2. Register the shared container and a ModelState on Application.
private func makeModelContainer() -> ModelContainer {
do {
return try ModelContainer(for: TodoItem.self)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
extension Application {
var modelContainer: Dependency<ModelContainer> {
modelContainer(makeModelContainer())
}
var todoItems: ModelState<TodoItem> {
modelState(
container: \.modelContainer,
fetchDescriptor: FetchDescriptor<TodoItem>(
sortBy: [SortDescriptor(\.title)]
),
id: "todoItems"
)
}
}
// 3. Use @ModelState from a view model.
@MainActor
final class TodoListViewModel: ObservableObject {
@ModelState(\.todoItems) var todoItems: [TodoItem]
func add(title: String) {
$todoItems.insert(TodoItem(title: title))
}
func toggle(_ item: TodoItem) {
item.isComplete.toggle()
$todoItems.save()
}
func remove(_ item: TodoItem) {
$todoItems.delete(item)
}
func clearAll() {
$todoItems.deleteAll()
}
}
Registering the ModelContainer
modelContainer(_:) registers the container with an auto-generated identifier and evaluates the autoclosure only once. Build the container in a helper rather than inline β it makes failures explicit:
extension Application {
var modelContainer: Dependency<ModelContainer> {
modelContainer(makeModelContainer())
}
}
Defining a ModelState
With no FetchDescriptor, the state matches all models of the given type:
extension Application {
var items: ModelState<Item> {
modelState(container: \.modelContainer)
}
}
Supply a FetchDescriptor for filtering or sorting:
extension Application {
var items: ModelState<Item> {
modelState(
container: \.modelContainer,
fetchDescriptor: FetchDescriptor<Item>(
sortBy: [SortDescriptor(\.title)]
),
id: "items"
)
}
}
Reading and Mutating
Via @ModelState β read the wrapped value, mutate through $items:
@ModelState(\.items) var items: [Item]
func add(_ item: Item) { $items.insert(item) }
func remove(_ item: Item) { $items.delete(item) }
func persist() { $items.save() }
Via Application.modelState β useful in services and non-view code:
@MainActor
func syncItems() {
let state = Application.modelState(\.items)
let current = state.models
state.insert(Item(title: "New"))
state.delete(current.first!)
state.save()
}
modelsperforms a live SwiftData fetch on every read. Capture the result in a local when you need it more than once.
Projected-value API
| Method | Behavior |
|---|---|
$items.insert(_:) | Inserts a model and saves |
$items.delete(_:) | Deletes a model and saves |
$items.save() | Persists pending changes |
$items.deleteAll() | Deletes all models matching the FetchDescriptor and saves |
These mutators log and swallow any underlying SwiftData error so call sites stay terse. When you need to surface or recover from a failed write, reach for the throwing counterparts on strict:
do {
try $items.strict.insert(item)
try $items.strict.save()
} catch {
// present the error, roll back, retryβ¦
}
strict exposes throwing versions of all four mutators (insert, delete, save, deleteAll) backed by the same context β pick the lenient API when a logged failure is acceptable, and strict when the caller must handle it.
Accessing the ModelContext
let context = Application.modelContext(\.modelContainer)
Returns the mainContext of the resolved ModelContainer β the same context used by all reads and writes.
ModelState vs SwiftData @Query
ModelState mutations are not automatically broadcast to SwiftUI views. This is intentional.
-
Reactive views β use
@Query. It observes theModelContextdirectly and refreshes the view when data changes. Share the AppState-provided container with the SwiftUI environment so views and non-view code use the same store:@main struct MyApp: App { var body: some Scene { WindowGroup { ItemsView() } .modelContainer(Application.dependency(\.modelContainer)) } } struct ItemsView: View { @Query(sort: \Item.title) private var items: [Item] var body: some View { List(items) { Text(\$0.title) } } } -
View models and services β use
@ModelState/Application.modelState. Ideal when@Environmentand@Queryaren't available, or when you need model operations outside of view code.
Notes
- All reads and writes go through the container's
mainContextβ keep usages on the main actor. ModelStatedoes not cache results in AppState's own cache. SwiftData'sModelContextis the source of truth.- Register a single
ModelContainerdependency and reference it from all model states and the SwiftUI environment.