Data Persistence with SwiftData

Mar 19 2025 · Swift 5.10, iOS 17, ipadOS 17, macOS 15, visionOS 1.2, Xcode 15

Lesson 01: Getting Started With Swift Data

Getting Started With SwiftData Demo

Episode complete

Play next episode

Next
Transcript

Open the GoodDog app from the Starter folder. It’s already been created as a multi-platform SwiftUI app but without any data storage. Build and Run the app in the Simulator. You’ll see that it already has an app icon, start screen, and displays a single record in the DogListView, SwiftUI file.

The app also has a DogModel Swift file, which sets up a dog name, age, weight, color, and breed. You’ll modify these properties as you go through the lessons, but for now, they’re either String or Int types, and some are Optional types.

Select the DogModel from the Project navigator, and in the Editor pane, add import SwiftData at the top of the file.

import SwiftData

Inside the DogModel class, add @Model to the class declaration line. You can either place the @Model macro on the start of the line or just above. Both are valid.

@Model
class DogModel { // ... }

or

@Model class DogModel { // ... }

Build the app with the Command-B or by choosing Build from the Project menu.

Pro Tip: when the compiler notices a change in the code, it will try to update the Canvas Preview. You can use Command-B to start the update as well.

Behind the scenes, SwiftData is setting the DogModel up as a SwiftData model. SwiftData sets up the Persistent Store, modelContainer, and modelContext with the properties defined in the model.

Note: Your model class will always require an init method for SwiftData, even if you provide default values. For the lessons here, you’ll name the models with the Model suffix for clarity. In common practice, the dog model would simply be named Dog and referred to in the app as a Dog or instance of the Dog model.

Select the GoodDogApp.swift file in the Project navigator. Import SwiftData at the top of the file. In the Window Group, just below the DogListView(), add the .modelContainer modifier. Here, you’ll list your models in an array. Add the following.

WindowGroup {
  DogListView()
    .modelContainer(for: DogModel.self)
}

Since you only have one model at the moment, you don’t need to add the square brackets. You’ve added the modelContainer to the DogListView. If you have more views in your WindowGroup, you can move the modelContainer down outside the WindowGroup to apply to the whole group of views.

Note: You’ll see later, when you add more models and create relationships with DogModel, that you won’t need to list all the models. That’s another nice thing about SwiftData. In some cases, you may need to list more models, but for most of these lessons, you won’t.

So far, you’ve created the model and modelContainer. Next, you would fetch the data from the data store and display it. Select the DogListView.swift from the Project navigator.

Import SwiftData at the top of the file. Inside the DogListView struct, add the @Query as a private var dogs, of type DogModel. Put the model in square brackets since you’re fetching multiple dogs here.

@Query private var dogs: [DogModel]

Imaginary Dogs with Mock Data

Currently, your data store has no data. To create some mock data for the Canvas preview, head back to the DogModel. Add an extension on the DogModel. At the top add @ModelActor, which is a special concurrency macro to have the preview actions run on the main thread.

Here, you’ll add a preview instance of the ModelContainer for DogModel.self and a ModelConfiguration setting to store in-memory. In other words, you’re creating data objects for the preview that are only persisted while the Canvas view is active. You can find the mock data code in Resources.txt in the Starter folder. Here you’re using the mainContext, a ModelContext type, which is a property of the ModelContainer. Add this to the bottom of the DogModel file, below the last curly brace.

extension DogModel {
  @MainActor
  static var preview: ModelContainer {
    let container = try! ModelContainer(
      for: DogModel.self,
      configurations: ModelConfiguration(isStoredInMemoryOnly: true))

    container.mainContext.insert(DogModel(
      name: "Mac",
      age: 11,
      weight: 90))
    container.mainContext.insert(DogModel(
      name: "Sorcha",
      age: 1,
      weight: 40))
    container.mainContext.insert(DogModel(
      name: "Violet",
      age: 4,
      weight: 85))
    container.mainContext.insert(DogModel(
      name: "Kirby",
      age: 10,
      weight: 95))

    return container
  }
}

Go back to DogListView and fetch that mock data. Embed the HStack inside a List, step through the fetched dogs, and return a dog object on each row. Replace the hard-coded string with the fetched dog’s name. You’ll add a dog.image in a later lesson.

List(dogs) { dog in
  HStack {
    Image(systemName: "dog")
      .imageScale(.large)
      .foregroundStyle(.tint)
    Text(dog.name)
  }
}

Wait a minute! The Canvas preview is now blank. You need to load the mock data for the preview. Add the modelContainer modifier to the DogListView() in #Preview, with an instance of .preview instead of .self.

#Preview {
  DogListView()
    .modelContainer(DogModel.preview)
}

Check it out. You’ve just fetched data with SwiftData and displayed in a List. Keep in mind that the modelContainer will set up the store, add the DogModel and make a context that you’ll use later.

This is one of many ways to display mock data, and you’ll use this often. There are cases where you’ll use DogModel.self and make the mock data directly in the preview, as you may have done in other apps.

New Dogs, New Tricks

If you run the app on the Simulator, you’ll still have an empty view. To remedy that, you’ll make use of a view to make new dogs and another view to edit the dog records. You’ll start by adding a NavigationStack to the view. Embed the List in a NavigationStack and set the .navigationTitle to “Good Dogs” as a modifier to the List.

NavigationStack {
  List(dogs) { dog in
  // ...
  }
  .navigationTitle("Good Dogs)
}

While you’re there, add a modifier to the HStack to use the .title font.

HStack {
  // ...
}
.font(.title)

Below the .navigationTitle, add a .toolbar with ToolbarItem containing an “Add New Dog” Button with systemImage plus. Leave the action closure empty for now.

.toolbar {
  ToolbarItem(placement:
   .primaryAction) {
    Button("Add New Dog",
    systemImage: "plus") {
      // action closure
    }
  }
}

From the Project navigator, select the NewDogView.swift. It’s been set up with a few State properties, and you’ll need to update it for SwiftData. Since this view isn’t going to be fetching data, you don’t need to import SwiftData. Instead, you’ll make use of the Environment property to connect to the modelContainer you added to DogListView in the App file. You’ll use the modelContext that SwiftData created for this. The modelContainer in the app file sets up the permanent storage, adds the model to the storage, and loads the model into the modelContext.

At the top of the NewDogView struct, add an instance of the environment modelContext.

@Environment(\.modelContext)
private var modelContext

Now you can fill out the action in the Create button to save your first dog. You do this by adding a newDog model with data values and insert the model. SwiftData will save the newDog into the persisted store. By default, SwiftData is configured to autosave changes in the modelContext. Later, you’ll see times where you explicitly call save, as you may have done in Core Data apps.

Add these two lines to the Create Button’s action:

let newDog = DogModel(name: name)
modelContext.insert(newDog)

Switch back to DogListView and add a State property to show the NewDogView with a default false value. When the Add button is tapped, you’ll toggle this value by setting it to true.

@State private var showingNewDogScreen = false

Add a .sheet(isPresented: ) below the .toolBar. When the $showingNewDogScreen is true, you’ll show the NewDogView as a medium size sheet.

.sheet(isPresented: $showingNewDogScreen) {
  NewDogView()
    .presentationDetents([.medium, .large])
}

Note: Here, the .large expands the sheet to full height when activated. You can leave this out if you like.

Now, when the compiler checks the build, you’ll see an error asking you to pass in name because it’s required. The preview will also fail. If you look through the Canvas preview diagnosis, you’ll see:

Compiling failed: missing arguments for parameters 'name' in call

While refactoring parts of a SwiftData app, you will encounter various errors. You will learn a few ways to fix the previews along the way.

Since this a new dog, those values aren’t known yet. Enter an empty string as the name value. The Create button on the NewDogView is disabled when the name field is empty, so this will work.

.sheet(isPresented: $showingNewDogScreen) {
  NewDogView(name: "")
    .presentationDetents(
      [.medium, .large]
    )
}

In the Add New Dog button, set the property to true.

Button("Add New Dog", systemImage: "plus") {
  showingNewDogScreen = true // add this
}

Now that the preview is fixed, you can try to create a new dog right in Xcode’s Canvas preview. From the DogListView, tap the + in the Canvas. The new dog sheet slides up. Enter Fido and press Create. Swipe down on the sheet to dismiss it. There’s the new dog created in memory and the modelContext has updated the List to show all the dogs. Nice!

It would also be nice if the sheet could dismiss when saving, too. Go back to NewDogView and add the @Environment’s dismiss function. The environment has many of these actions available. At the top of the struct, add the following:

@Environment(\.dismiss) private var dismiss

Then, under the modelContext, insert the call to dismiss. The button should now look like this.

Button("Create") {
  let newDog = DogModel(name: name)
  modelContext.insert(newDog)
  dismiss()
}

Also, add dismiss() to the Cancel button action in case your user changes their mind.

Build and Run in the Simulator, and make a dog or two. Stop the Simulator in Xcode, and run the app again.

Check it out! The dog you created is now persisted in the store, fetched, and displayed in the List, as if by magic.

Visiting the Vet - Debugging Data

Putting the magic aside, debugging SwiftData and Core Data can seem like a magical mystery. However, there are several apps that let you take a peek at the data stores. For instance, you can use DB Browser for SQLite. You may wonder where the data is stored. You can use the Mac’s System Services to find it.

Select the GoodDogApp.swift from the Project navigator. Add an init method to show the path to the store in the debug console. Add the following at the bottom of the app struct:

init() {
  print(URL.applicationSupportDirectory.path(percentEncoded: false))
}

Run the app in the Simulator. From the console, select the printed path up to the Library. The space character in the Application Support breaks the trick, so don’t include it. Right-click on the selected path, and from the contextual menu, choose Services and Open. The Mac will prompt you to confirm, so choose Run Service.

The folder inside the Simulator’s app opens up. Open the Application Support folder, and you should see three files:

default.store
default.store-shm
default.store-wam

They’re the same file types you get with a Core Data app. The name might be different, but you’ll see how to make your own name later.

Pro Tip: While in development, you might need to alter your data set. If you like the state of a set of records in the Simulator, you can copy and store the three files somewhere on your Mac. If you want to put them back, stop your app and put these files back in. Run the app in the Simulator, and your records should be back. This will only work until you make a big change to the data. If so, the app might crash in the Simulator. If that happens, delete the app from the Simulator and build a new version.

Select the default.store, right-click and choose Open With > Other. In the Applications folder that opens, choose DB Browser for SQLite or your Core Data tool of choice.

In DB Browser for SQLite, choose the Browse Data tab. From the Table selector, choose ZDOGMODEL. You should see the dogs you created in the app here. Z_PK is the primary key, and you should see ZNAME, listing the names of the records you made.

Neat huh? This will be super helpful when debugging your SwiftData and Core Data apps.

Deleting Bad Dogs

Along with creating new records, SwiftData can also delete them. It’s actually very straightforward.

@Query by itself doesn’t have a delete function. You’ll need to add another modelContext from the Environment at the top of the DogListView struct.

@Environment(\.modelContext) private var modelContext

You’ll need to refactor the List to contain a ForEach, since List doesn’t have an .onDelete method. Change the List to a ForEach and embed that in a plain List. After that at the bottom curly brace of the Foreach add an .onDelete(perform: ). You’ll call a func called dogToDelete, which you’ll add after this.

List {
  ForEach(dogs) { dog in
  // ...
  }
  .onDelete(perform: dogToDelete)
}

Pro Tip: If you double click the opening curly brace, Xcode will select the whole code block. That makes it easy to find the closing brace.

Add a func dogToDelete inside the DogListView struct, to delete the dog at the List’s row index. Remember that the PersistentModel protocol is Identifiable.

func dogToDelete(indexSet: IndexSet) {
  for index in indexSet {
    modelContext.delete(dogs[index])
  }
}

Since you now have mock data, you can test out the swipe to delete with the Canvas view. Don’t worry, the mock data will return the next time you build this view. You can also run the app in the Simulator and try out the deletion.

Note: You can also build and run on a device. Just change the Bundle ID to your own team’s bundle ID in Signing & Capabilities.

Dog Training 101 - Editing Dogs

In the Project Navigator, select the EditDogView. Notice that it’s already been set up with state properties similar to the model. It also has a PhotoUI PhotosPicker already set up. There’s also a computed variable called changed, so if any of the properties is changed, an Update button will appear in the toolbar. Next, you’ll set this view up for SwiftData.

At the top of the file import SwiftData:

import SwiftData

After the Environment dismiss, add a bindable DogModel with the following.

@Bindable var dog: DogModel

The preview will complain, but you’ll fix that soon.

Now, to load the data into the fields, you’ll use .onAppear. After the navigation modifiers, add the onAppear with each property in DogModel, with nil-coalescing operator for the Optional values. Also, add a pragma mark to make this easier to find later as this view may grow.While your there add didAppear = true to make the Showing the Update button smoother.

// MARK: onAppear
.onAppear {
  name = dog.name
  age = dog.age ?? 0
  weight = dog.weight ?? 0
  color = dog.color ?? ""
  breed = dog.breed ?? ""

  didAppear = true
}

To fix the preview, you’ll need to provide local mock data. Start by adding a modelContainer with DogModel inside the preview. Then, make a dog with values. Lastly, return the EditDogView with the container you made in the preview. Your preview will look like this:

#Preview {
  let container = try! ModelContainer(for: DogModel.self)
  let dog = DogModel(
    name: "Mac",
    age: 11,
    weight: 90,
    color: "Yellow",
    breed: "Labrador Retriever")

  return EditDogView(dog: dog)
    .modelContainer(container)
}

Now that EditDogView is ready, head back to DogListView and add a NavigationLink(value:label:) to the items in the Foreach. EditDogView goes in the first closure, and the existing HStack goes in the label: closure. In EditDogView call, you’ll pass in the selected dog:

NavigationLink {
  EditDogView(dog: dog)
} label: {
  HStack {
    Image(systemName: "dog")
      .imageScale(.large)
      .foregroundStyle(.tint)
    Text(dog.name)
  }
  .font(.title)
}

Notice that a disclosure indicator is added to each row. Go ahead and try it out in the Canvas preview. The dog you tap should load into the EditDogView. The Update button still doesn’t appear when you change some values. To fix that, you need to change the changed variable to watch for changes to the dog model object. Update the changed variable.

var changed: Bool {
  name != dog.name
  || age != dog.age
  || weight != dog.weight
  || color != dog.color
  || breed != dog.breed
}

The last step is to assign the changed values and save when the Update Button is tapped. Remember, autosave is enabled, so the data changes will be saved from the context.

In the Update button, assign the field values to the dog object before the dismiss():

Button("Update") {
  dog.name = name
  dog.age = age
  dog.weight = weight
  dog.color = color
  dog.breed = breed
  dismiss()
}

Doggy Picture Day

You’ve accomplished quite a lot so far, but you may be wondering about the Photo picker. To handle images, you’ll add a Data property to the model. Since images can be large, you’ll also use the @Attribute for external storage. SwiftData uses Data for binary types.

In the DogModel, add an image var of type Data and add the attribute with the externalStorage setting.

@Attribute(.externalStorage) var image: Data?

Add the image to the initializer with a nil value.

, image: Data? = nil // NB add comma
}
// ...
self.image = image
}

Behind the scenes, SwiftData will perform a migration on the model changes to add an image property.

You’ll also need to fix the mock data with a nil value for the images. Add , image: nil to each row. You’ll see how to mock images in a future lesson.

container.mainContext.insert(
  DogModel(
    name: "Mac",
    age: 11,
    weight: 90,
    image: nil))
container.mainContext.insert(
  DogModel(
    name: "Sorcha",
    age: 1,
    weight: 40,
    image: nil))
container.mainContext.insert(
  DogModel(
    name: "Violet",
    age: 4,
    weight: 85,
    image: nil))
container.mainContext.insert(
  DogModel(
    name: "Kirby",
    age: 10,
    weight: 95,
    image: nil))

In the EditDogView, there’s already an image property as a State variable. Add the image in the changed variable, in the onAppear, and in the Update button.

// var changed Bool
|| image != dog.image

// .onAppear {
image = dog.image

// Button("Update") {
dog.image = image // before dismiss()

Finally, add a .task to use the photo picker to convert the image to Data asynchronously and assign it to the dog object’s image property. Put this code near where the .onAppear and .toolbar are located.

.task(id: selectedPhoto) {
 // the photo picker has a protocol to convert to Data type
 if let data = try? await selectedPhoto?.loadTransferable(type: Data.self) {
   image = data
 }
}

Phew! That’s a lot of work you’ve done. You’ve only scratched the surface of using SwiftData for CRUD operations. You learned how to fetch real and mock data with SwiftData, how to create records, delete records, and how to update records. As a bonus, you learned to add images to your data objects.

Congratulations on making a working SwiftData app. Now, continue to the next part for a summary.

See forum comments
Cinema mode Download course materials from Github
Previous: Getting Started With SwiftData Next: Getting Started With SwiftData Conclusion