Modeling data with @Modelswift-6.4/ios-26
Lesson 1 / 5
Unit 11 · SwiftData persistence

Modeling data with @Model

Coming from
Not covered in this lesson — showing TypeScript.
Note

Unit 11 · SwiftData persistence. This unit is prose-and-code: SwiftData needs a running store, so it can't execute in the graded sandbox. Read the patterns carefully — you'll wire this into Checklist (Project 2) and every data-backed project after.

SwiftData is Apple's modern persistence framework. You annotate a normal-looking class with @Model, register it with the app once, and the framework gives you a database: objects you insert are saved, survive relaunch, and can be queried and observed by SwiftUI. There's no schema file to edit and no boilerplate fetch code — the macro generates it.

Defining a model

@Model turns a class into a persisted type:

import SwiftData

@Model
final class Task {
    var title: String
    var isDone: Bool
    var createdAt: Date

    init(title: String, isDone: Bool = false, createdAt: Date = .now) {
        self.title = title
        self.isDone = isDone
        self.createdAt = createdAt
    }
}

A few rules the macro imposes. It must be a final class, not a struct — persistence needs reference identity so that a change to one instance is the same row everywhere. Every stored var becomes a persisted attribute automatically; you write no column definitions. And you provide an init as usual. Under the hood @Model rewrites each property to read and write through the store, but your call sites look like ordinary property access.

Tip

@Model is a class, but this is not a return to reference-type modeling. You still prefer structs for plain data (Unit 1). A @Model class earns its reference semantics because a persisted record has identity — the same row, observed in many views, must reflect one edit everywhere. That's the deliberate "shared mutable identity" case structs don't cover.

Registering the container

The app needs exactly one model container — the actual database, backing every context. Register it at the app root with .modelContainer(for:):

import SwiftUI
import SwiftData

@main
struct ChecklistApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Task.self)
    }
}

That single modifier creates the store for Task (list multiple types in an array), and injects a main-actor ModelContext into the environment for every view below it. You now have persistence with three lines and no configuration file.

Reaching the context and mutating it

A view reads the context from the environment:

struct AddTaskButton: View {
    @Environment(\.modelContext) private var context

    var body: some View {
        Button("Add") {
            let task = Task(title: "New task")
            context.insert(task)     // staged into the context
            // context.save() is usually implicit — see below
        }
    }
}

The ModelContext is your unit of work. Its core operations:

  • context.insert(object) — start tracking a new object for persistence.
  • context.delete(object) — remove it from the store.
  • context.save() — flush pending changes to disk.

In a SwiftUI app the environment's context autosaves by default — changes are written on the next run-loop tick — so you rarely call save() explicitly. Call it yourself when you need a write to complete now (before a critical navigation, say) or when you've turned autosave off.

Coming from TypeScript

If you've used an ORM like Prisma or TypeORM, @Model is the entity decorator and the ModelContext is the session/unit-of-work you insert and save through. The difference is there's no separate schema migration file to generate for the basics — the macro derives the store shape from the class, and SwiftUI observes it directly, so an insert re-renders the list without a manual refetch.

Knowledge check

Q: Why must a @Model type be a class and not a struct? Persistence needs stable identity — the same stored record, observed from many places, must reflect a single edit everywhere. That's reference semantics, which only a class provides. It's the deliberate exception to "struct by default."

Q: You insert an object in a SwiftUI view and never call save(). Is it persisted? Yes, in the normal case. The environment's ModelContext autosaves, flushing pending changes on the next tick. You call save() explicitly only to force an immediate write or when autosave is disabled.