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— SwiftDataNote: 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?
- Defining a Model
- Setting Up the ModelContainer
- Creating and Saving Data
- Querying Data with @Query
- Updating and Deleting Data
- Common Mistakes
- What’s Next?
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
@Queryto work, your model must conform toIdentifiable. Adding anidproperty is one way, but@Modelclasses get implicit identity through SwiftData, so theListworks without an explicitidparameter.
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
@Modelon a class to define a persistent data type - Attach
.modelContainer(for:)to your app to set up the database - Use
@Queryto fetch and display data — it automatically stays in sync - Insert with
context.insert(), update by mutating properties, and delete withcontext.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.