Getting Started with SwiftData: Your First Persistent Model


WALL-E spent 700 years collecting treasures and carefully organizing them in his truck. If he lost power and forgot where everything was, centuries of work would be gone. Your app needs the same kind of reliable, organized storage — and SwiftData is how you build it.

SwiftData is Apple’s modern framework for storing structured data that persists across app launches. You’ll learn how to define a data model with @Model, set up a ModelContainer, perform create-read-update-delete (CRUD) operations, and display persistent data in SwiftUI using @Query. We won’t cover relationships between models or migrations — those each get their own posts.

Apple Docs: @Model — SwiftData

Note: SwiftData requires iOS 17 or later. If you need to support older iOS versions, you’ll need to use Core Data instead.

This guide assumes you’re comfortable with persistence basics and SwiftUI state management.

What You’ll Learn

What Is SwiftData?

SwiftData is a persistence framework — a tool for saving structured data (like a list of movies or characters) to disk so it’s still there when the user closes and reopens your app.

Think of the difference between UserDefaults and SwiftData like this: UserDefaults is a sticky note on your fridge (“Buy milk”), while SwiftData is the entire filing cabinet at Pixar Studios — organized, searchable, and able to handle thousands of records.

SwiftData uses Swift macros to reduce boilerplate code. Instead of writing configuration files or mapping schemas, you just add @Model to a class and SwiftData figures out the rest.

Defining a Model

A model is a Swift class that describes the shape of your data. To make it work with SwiftData, add the @Model macro above the class definition.

import SwiftData

@Model
class Movie {
    var title: String
    var year: Int
    var rating: Double
    var isWatched: Bool

    init(title: String, year: Int, rating: Double, isWatched: Bool = false) {
        self.title = title
        self.year = year
        self.rating = rating
        self.isWatched = isWatched
    }
}

The @Model macro tells SwiftData to track this class for persistence. Every property becomes a stored column in the underlying database. Notice this is a class, not a struct — SwiftData models must be classes because the framework needs to track changes by reference.

Tip: You can use String, Int, Double, Bool, Date, Data, arrays, and optionals as model properties. SwiftData handles the conversion to and from storage automatically.

Setting Up the ModelContainer

Before SwiftData can save or load anything, your app needs a ModelContainer — the object that manages the database connection and holds your data.

Apple Docs: ModelContainer — SwiftData

The simplest setup is one line in your App struct:

import SwiftUI
import SwiftData

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

The .modelContainer(for: Movie.self) modifier creates a database that can store Movie objects and makes it available to every view in your app. SwiftData creates the database file automatically — you don’t need to manage any files yourself.

Creating and Saving Data

To insert a new record, you create a model instance and pass it to the model context. The model context is SwiftData’s workspace — it tracks all changes you make and saves them to disk.

import SwiftUI
import SwiftData

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

    var body: some View {
        Button("Add Toy Story") {
            let movie = Movie(
                title: "Toy Story",
                year: 1995,
                rating: 9.0
            )
            context.insert(movie)
        }
    }
}

When you call context.insert(movie), SwiftData adds the movie to the database. By default, SwiftData autosaves — you don’t need to call a separate save method. The data is persisted automatically at appropriate moments.

Querying Data with @Query

@Query is a property wrapper that automatically fetches data from SwiftData and keeps your view updated when the data changes.

Apple Docs: @Query — SwiftData

struct MovieListView: View {
    @Query private var movies: [Movie]

    var body: some View {
        List(movies) { movie in
            VStack(alignment: .leading) {
                Text(movie.title)
                    .font(.headline)
                Text("\(movie.year) — ⭐ \(movie.rating, specifier: "%.1f")")
                    .font(.caption)
            }
        }
    }
}

Just by declaring @Query private var movies: [Movie], SwiftData fetches all Movie records and updates the list whenever a movie is added, changed, or deleted.

You can also sort and filter queries directly:

@Query(sort: \Movie.year, order: .reverse)
private var movies: [Movie]

This returns movies sorted by year, newest first. No extra code needed — SwiftData handles it all behind the scenes.

Note: For @Query to work, your model must conform to Identifiable. Adding an id property is one way, but @Model classes get implicit identity through SwiftData, so the List works without an explicit id parameter.

Updating and Deleting Data

Since SwiftData models are classes, updating a record is as simple as changing a property. SwiftData detects the change and saves it automatically.

struct MovieDetailView: View {
    let movie: Movie
    @Environment(\.modelContext) private var context

    var body: some View {
        VStack {
            Text(movie.title)
            Toggle("Watched", isOn: Binding(
                get: { movie.isWatched },
                set: { movie.isWatched = $0 }
            ))

            Button("Delete Movie", role: .destructive) {
                context.delete(movie)
            }
        }
    }
}

Toggling isWatched directly mutates the model object, and SwiftData automatically persists the change. To delete a record, call context.delete(movie). The object is removed from the database on the next autosave.

Common Mistakes

Using a Struct Instead of a Class

SwiftData models must be classes. The @Model macro doesn’t work on structs because SwiftData needs reference semantics to track changes.

// ❌ Don't do this — structs can't be SwiftData models
@Model
struct Movie {
    var title: String
}
// ✅ Do this instead — use a class
@Model
class Movie {
    var title: String

    init(title: String) {
        self.title = title
    }
}

Forgetting the ModelContainer

If you forget to attach .modelContainer(for:) to your app or scene, @Query will have nothing to fetch from and @Environment(\.modelContext) will crash at runtime.

// ❌ Don't do this — no container set up
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView() // @Query won't work!
        }
    }
}
// ✅ Do this instead — attach the container
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Movie.self)
    }
}

Trying to Save Manually on Every Change

SwiftData autosaves for you. Calling try context.save() after every single change is unnecessary and can hurt performance.

// ❌ Don't do this — unnecessary manual saves
context.insert(movie)
try? context.save() // Not needed!
// ✅ Do this instead — let SwiftData autosave
context.insert(movie)
// SwiftData saves automatically

What’s Next?

  • SwiftData is Apple’s modern persistence framework, built on Swift macros
  • Use @Model on a class to define a persistent data type
  • Attach .modelContainer(for:) to your app to set up the database
  • Use @Query to fetch and display data — it automatically stays in sync
  • Insert with context.insert(), update by mutating properties, and delete with context.delete()
  • SwiftData autosaves, so you rarely need to call save manually

Ready to connect your models together? Head over to SwiftData Relationships to learn how to model connections between objects — like linking characters to the movies they appear in.