Data Persistence with SwiftData

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

Lesson 02: SwiftData & SwiftUI Integration

One to Many Relationships Demo

Episode complete

Play next episode

Next
Transcript

Making Connections - Relationships

The real power of SwiftData shines when you add relationships between models. Relationships provide for data that can uniquely associated, or data that’s easily shared with multiple records, and also helps eliminate input errors.

You’ll recall that there are already two new models added, a BreedModel and a ParkModel. It will be good practice to convert these to support SwiftData and make relationships with the DogModel.

Select the BreedModel from the Project Navigator. At the top, add import SwiftData. As you’ve done before add @Model to the BreedModel class. Next you’ll add a reference to the DogModel. Each breed can be connected to many dogs, so this is a one-to-many relationship. Change the class to look like this.

import Foundation
import SwiftData

@Model
class BreedModel {
  var name: String
  var dogs: [DogModel]?

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

Here you’ve added and a array of dogs of DogModel type. Run a build with Command-B and SwiftData will prepare and set up the model in the modelContainer. Next select the DogModel and add the BreedModel on the dog side. Change the var breed type to BreedModel and remove the empty string.

@Model
class DogModel {
  //...

  // 1. change breed type to BreedModel
  var breed: BreedModel?

  init(
    // ...

    // 2. change breed type to BreedModel
    breed: BreedModel? = nil,
    // ...
  ) {
    // ...

    self.breed = breed  // no change
    // ...
  }
}

Recall that SwiftData sets up the inverse relationship by default. Optionally, you could indicate the relationship with the @Relationship macro. It would look like this if you did.

// in the DogModel - optional
@Relationship(inverse: \BreedModel.name)
var breed: BreedModel?

// in the BreedModel
@Relationship
var dogs: [DogModel]?

Notice that the inverse: is only set on one side of the relationship. You don’t add the .inverse to a property on both sides, otherwise the compiler will warn you about a circular reference. SwiftData will infer the inverse relationship. In a later lesson, you will need to add the implicit type of relationship. For now you can skip this.

Note: Recall that in the GoodDogApp.swift file the modelContainer takes an array of SwiftData models. Currently the modelContainer only contains DogModel.self. You could put in a array of models like [DogModel.self, BreedModel.self], but since you’ve established the relationship, only DogModel.self is required.

Now that the BreedModel relationship has been added to the DogModel, you’ll need to refactor the mock data in the DogModel extension. Breed is no longer a string. Make the following adjustments. You’ll make each dog into it’s own variable and a variable for each type of breed. Then add a breed to each dog.

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

    // breeds
    let labrador = BreedModel(name: "Labrador Retriever")
    let golden = BreedModel(name: "Golden Retriever")
    let bouvier = BreedModel(name: "Bouvier")
    let mixed = BreedModel(name: "Mixed")

    // dogs
    let macDog = DogModel(
      name: "Mac",
      age: 11,
      weight: 90,
      color: "Yellow",
      breed: labrador,
      image: nil
    )
    let sorcha = DogModel(
      name: "Sorcha",
      age: 1,
      weight: 40,
      color: "Yellow",
      breed: golden,
      image: nil
    )
    let violet = DogModel(
      name: "Violet",
      age: 4,
      weight: 85,
      color: "Gray",
      breed: bouvier,
      image: nil
    )
    let kirby = DogModel(
      name: "Kirby",
      age: 11,
      weight: 95,
      color: "Fox Red",
      breed: labrador,
      image: nil
    )
    let priscilla = DogModel(
      name: "Priscilla",
      age: 17,
      weight: 65,
      color: "White",
      breed: mixed,
      image: nil
    )

    container.mainContext.insert(macDog)
    container.mainContext.insert(sorcha)
    container.mainContext.insert(violet)
    container.mainContext.insert(kirby)
    container.mainContext.insert(priscilla)

    return container
  }
}

Now that the mock data is ready you can start to work on the views. Select DogList from the Project Navigator. The breed predicate is now broken since you have moved the breed name to the BreedModel. Update the predicate to use breed.name.

dog.breed?.name.localizedStandardContains(filterString) ?? false

The breed is Optional so you’ll need to use optional chaining.

Select the EditDogView from the Project Navigator. Change the breed state variable to BreedModel Optional type.

@State private var breed: BreedModel?

At the bottom of the LabelContent, find the breed error and comment out the Text. You will add a Picker for breed soon.

LabeledContent {
//  TextField("", text: $breed)
} label: {
  Text("Breed")
    .foregroundStyle(.secondary)
}

Scroll down to the .onAppear and remove the default empty string on breed.

.onAppear {
  name = dog.name
  age = dog.age ?? 0
  weight = dog.weight ?? 0
  color = dog.color ?? ""
  breed = dog.breed
  image = dog.image
}

Scroll down to the Preview and remove the breed from the mock dog.

// #Preview
let dog = DogModel(
  name: "Mac",
  age: 11,
  weight: 90,
  color: "Yellow"
)

Now your Canvas previews should work again. Switch back to the DogList. select the age Text in the ForEach, Control-click and embed it in an HStack. Add a Text() to display the breed. Use optional chaining and a default empty string.

HStack {
  Text("age: \(String(describing: dog.age ?? 0))")
  Text("breed: \(String(describing: dog.breed?.name ?? ""))")
}
.font(.footnote)

Move the font styling to the HStack as well. Now you’ll make a Breed picker. Select the Views group in the Project Navigator and add a new SwiftUI file named BreedPicker.

At the top of the file, add import SwiftData. At the top of the BreedPicker struct add a Query using the BreedModel to fetch the breeds, sorting on the breed name.

@Query(sort: \BreedModel.name) private var breeds: [BreedModel]

Add a Binding variable called selectedBreed.

@Binding var selectedBreed: BreedModel?

This upsets the Preview again. You’ll make a local mock dog here, as you did on the EditDogView preview. Set up a modelContainer with the DogModel and then pass in a selectedBreed with .constant breed. Replace the Preview with this code.

#Preview {
  let container = try! ModelContainer(for: DogModel.self)
  let selectedBreed = Binding<BreedModel?>.constant(BreedModel(name: "Labrador Retriever"))
  return BreedPicker(selectedBreed: selectedBreed)
    .modelContainer(container)
}

Now add the picker to the view. Replace the boilerplate code with this.

var body: some View {
  Picker("Breed", selection: $selectedBreed) {
    Text("Select breed").tag(nil as BreedModel?)
    ForEach(breeds) { breed in
      Text(breed.name).tag(Optional(breed))
    }
  }
  .buttonStyle(.bordered)
}

You can now add the BreedPicker back into the EditDogView, where you had commented out the breed Textfield.

LabeledContent {
  BreedPicker(selectedBreed: $breed)
} label: {
  Text("Breed")
    .foregroundStyle(.secondary)
}

List of Breeds

Next you’ll update the BreedListView view for SwiftData. This is similar to the DogList. Select the BreedListView from the Project Navigator, and import SwiftData at the top.

At the top of the BreedListView struct add the Environment’s modelContext and a breed Query sorting on the breed name.

@Environment(\.modelContext) private var modelContext
@Query(sort: \BreedModel.name) private var breeds: [BreedModel]

Update the Preview with a modelContainer to fetch the mock data from DogModel.

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

Update the List’s ForEach to use the breeds data and show the breed name.

ForEach(breeds) { breed in
  Text(breed.name)
}

Below the View’s closing curly brace add a function to delete the breed at index.

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

Add the .onDelete() after the ForEach.

.onDelete(perform: breedToDelete)

Add a Breed

You’ll need a way to add breeds to the app. Add a button to navigate the the BreedList. At the top of the EditDogView struct there is a state variable to show the breed list.

@State private var showBreeds = false

Refactor the LabelledContent for breeds into an HStack containing the BreedPicker. Add a button below the BreedPicker. Label the button Edit Breeds and set the showBreeds to true.

HStack {
  BreedPicker(selectedBreed: $breed)
  Button("Edit Breeds") {
    showBreeds = true
  }
  .buttonStyle(.borderedProminent)
}

At the bottom of the VStack, add a .sheet(isPresented:) to show the BreedListView.

.sheet(isPresented: $showBreeds) {
  BreedListView()
    .presentationDetents([.large])
}

If you navigate in the Canvas Preview or in the Simulator, you’ll find that the BreedList is empty. You’ll now create a way to add a new breed.

The BreedListView already has a toolbar with a create button, that will go to the NewBreedView, however it’s not prepared for SwiftData.

Select NewBreedView.swift and import SwiftData at the top of the file. This file will be navigated to, so it only needs the modelContext to save to. Add the Environment modelContext at the top of the struct.

@Environment(\.modelContext) private var modelContext

Next add the save function to the Add Breed Button.

let newBreed = BreedModel(name: name)
modelContext.insert(newBreed)
try? modelContext.save()

Notice the line try? modelContext.save(). What this doing is explicitly calling the modelContext’s save(). If the ModelConfiguration was not set to autosave, you would need to call this save whenever you insert or update objects in the context. Thank you auto-save.

Walk the Dog

Now that you’ve added SwiftData you can try out adding a new breed. Select the DogListView and start the Canvas Preview. Click to select a dog. Notice that the mock data is loading the breed in the picker. Select a different breed and press Update. Marvel at how the breed has changed.

Build and Run the app in the Simulator and add some breeds. Assign some breeds to your dogs with the picker and save them.

As you did in the previous lesson, select the printed path from the console up to the Library. Remember the space character in the Application Support breaks the trick, so don’t include it in your selection. Control 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 in the Finder. Open the Application Support folder and you should see three files. Select the default.store, right click an 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 ZBREEDMODEL. You should see the breeds you added. Make note of the integer value in the Z_PK. Select ZDOGMODEL and you should see a field named ZBREED and the integer value should match the breed’s Z_PK. SwiftData like CoreData uses SQLite as a backing store by default.

Coming back to the app, you’ll now set up the EditBreedView. In the BreedListView, add a NavigationLink to the Text(breed.name).

NavigationLink{
  EditBreedView()
} label: {
  Text(breed.name)
}

This makes the rows selectable and navigates to the EditBreedView. Open the EditBreedView file and add a modelContext to the EditBreedView struct. Import SwiftData at the top of the file.

import SwiftData

At the top of the EditBreedView struct add a Bindable breed variable.

@Bindable var breed: BreedModel

Also add a computed variable onChanged to enable the Update button.

var changed: Bool {
  name != breed.name
}

Fix the Preview like you did in BreedPicker and EditDogView. Replace the Preview with this code.

#Preview {
  let container = try! ModelContainer(for:
      DogModel.self)
  let breed = BreedModel(name: "Labrador")
  return EditBreedView(breed: breed)
    .modelContainer(container)
}

The BreedModel is already bindable so you use that. However the BreedListView is now broken because you need pass in the selected breed. Update the call to EditBreedView() in BreedListView by adding the breed object.

EditBreedView(breed: breed)

You’re passing in the breed, now you’ll need to update name value. This time use .task instead of .onAppear. Select EditBreedView and add the code just under the .navigationTitle() which is modifying the GroupBox.

.task {
  name = breed.name
}

Wow! As soon as the preview updates, the .task makes the mock data breed appear. Nice! Now you’ll add the new breed name assignment to the button. Add the if changed { /... } to the button’s action closure above the dismiss.

Button ("Update Breed") {
  if changed {
    breed.name = name
  }
  dismiss()
}

There you have it. Just like meeting new dogs at the park, you are creating new relationships.

You can take a break and when you’re ready move on to learning about Many to Many Relationships in the next demo.

See forum comments
Cinema mode Download course materials from Github
Previous: Sort Filter Demo Next: Many to Many Relationships Demo