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

Many to Many Relationships Demo

Episode complete

Play next episode

Next

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Visit the Dog Parks

A one-to-one relationship would be like having a model for dog licenses. Each dog could have a unique license. The licenses could be managed in other functions, so it’s like adding the field in the DogModel directly.

import Foundation
import SwiftData

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

  init(name: String, dogs: [DogModel]? = nil) {
    self.name = name
    self.dogs = dogs
  }
}
@Model
class DogModel {
  // ...

  // 1. add the parks under the vars
  var parks: [ParkModel]?

  init(
    // ...

    // 2. add the parks here
    parks: [ParkModel]? = nil
  ) {
    // ...

    // 3. and here
    self.parks = parks
  }
}

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

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

    let riverdale = ParkModel(name: "Riverdale Park")
    let withrow = ParkModel(name: "Withrow Park")
    let greenwood = ParkModel(name: "Greewood Park")
    let hideaway = ParkModel(name: "Hideaway Park")
    let kewBeach = ParkModel(name: "Kew Beach Off Leash Dog Park")
    let allan = ParkModel(name: "Allan Gardens")

    let macDog = DogModel(
      name: "Mac",
      age: 11,
      weight: 90,
      color: "Yellow",
      breed: labrador,
      image: UIImage(
        resource: .macintosh
      ).pngData()!,
      parks: [
        riverdale,
        withrow,
        kewBeach
      ]
    )
    let sorcha = DogModel(
      name: "Sorcha",
      age: 1,
      weight: 40,
      color: "Yellow",
      breed: golden,
      image: UIImage(
        resource: .sorcha
      ).pngData()!,
      parks: [
        greenwood,
        withrow
      ]
    )
    let violet = DogModel(
      name: "Violet",
      age: 4,
      weight: 85,
      color: "Gray",
      breed: bouvier,
      image: UIImage(
        resource: .violet
      ).pngData()!,
      parks: [
        riverdale,
        withrow,
        hideaway
      ]
    )
    let kirby = DogModel(
      name: "Kirby",
      age: 11,
      weight: 95,
      color: "Fox Red",
      breed: labrador,
      image: UIImage(
        resource: .kirby
      ).pngData()!,
      parks: [
        allan,
        greenwood,
        kewBeach
      ]
    )
    let priscilla = DogModel(
      name: "Priscilla",
      age: 17,
      weight: 65,
      color: "White",
      breed: mixed,
      image: nil,
      parks: []
    )


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

    return container
  }
}
#if !os(macOS)
import UIKit
#endif
let dog = DogModel(
  name: "Mac",
  age: 11,
  weight: 90,
  color: "Yellow",
  image: UIImage(
    resource: .macintosh).pngData()!
)

Setting up the Parks

The next steps are similar to what you’ve done before the set up the views to support SwiftData. Select the ParksView, add import SwiftData and at the top of the main struct add a modelContext, a @Query sorting the park names, and a @Bindable dog to save the changes.

// at the top
import SwiftData

// in the ParksView struct
@Environment(\.modelContext) private var modelContext
@Query(sort: \ParkModel.name) var parks: [ParkModel]
@Bindable var dog: DogModel
#Preview {
  let container = try! ModelContainer(for: DogModel.self)
  let dog = DogModel(name: "Mac", parks: [])
  return ParksView(dog: dog)
    .modelContainer(container)
}
ForEach(parks) { park in
  Text(park.name)
}
Group {
  if !parks.isEmpty {
    List {
      ForEach(parks) { park in
        Text(park.name)
      }
    }
    LabeledContent {
      Button {
       // addRemove() will go here
      } label: {
        Image(systemName: "plus.circle.fill")
          .imageScale(.large)
      }
      .buttonStyle(.borderedProminent)
    } label: {
      Text("Create new park")
        .font(.caption)
        .foregroundStyle(.secondary)
    }
  } else {
    ContentUnavailableView {
      Image(systemName: "tree")
    } description: {
      Text("You need to create some parks.")
    } actions: {
      Button("Create Park") {
        newPark.toggle()
      }
      .buttonStyle(.borderedProminent)
    }
  }
}
// at the top of the file
import SwiftData

// at the top of the main struct
@Environment(\.modelContext) var modelContext
Button("Add Park") {
  let newPark = ParkModel(name: name)
  modelContext.insert(newPark)
  try? modelContext.save()
  dismiss()
}

Add and Remove Associated Parks

In a Many-to-Many relationship you can think of the objects as being associated with each other. To save the association on a particular dog, you can add and remove the park associations with each save. To accomplish this, add a func named addRemove() after the body view's closing curly braces.

func addRemove(_ park: ParkModel) {
  if let dogParks = dog.parks {
    // check if parks is empty
    if dogParks.isEmpty {
      dog.parks?.append(park)
    } else {
      // check if park is associated
      // remove park if true
      // add park if false
      if dogParks.contains(park),
          let index = dogParks.firstIndex(where: {
            $0.id == park.id
          }) {
        dog.parks?.remove(at: index)
      } else {
        dog.parks?.append(park)
      }
    }
  }
}
ForEach(parks) { park in
  HStack {
    if let dogParks = dog.parks {
      if dogParks.isEmpty {
        Button {
          addRemove(park)
        } label: {
          Image(systemName: "circle")
        }
      } else {
        Button {
          addRemove(park)
        } label: {
          Image(
            systemName:
              dogParks.contains(
                park
            ) ? "circle.fill" : "circle"
          )
        }
      }
    }
    Text(park.name)
  }
}
@State private var showParks = false
VStack {
  Button("Parks", systemImage: "tree") {
    showParks.toggle()
  }
  .buttonStyle(.borderedProminent)
}
.sheet(isPresented: $showParks) {
  ParksView(dog: dog)
    .presentationDetents([.large])
}
Button {
  // newPark.toggle will go here
  newPark.toggle()
} label: {
  Image(systemName: "plus.circle.fill")
    .imageScale(.large)
}

Leaving the Park

The last bit of functionality for the ParksView is to delete a park. Again, you will find the park at the index and remove it. Add the onDelete() to the bottom of the ForEach after the closing curly brace. Recall that the default delete rule is nullify. That means you can delete a park, but the dog will be persisted. Also when you delete a dog, the parks are persisted.

.onDelete(perform: { indexSet in
  // find the park at index
  indexSet.forEach { index in
    // 2. clear the local park here in the view
    if let dogParks = dog.parks,
        dogParks.contains(parks[index]),
        let dogParkIndex = dogParks.firstIndex(
          where: { $0.id == parks[index].id }
        ) {
          dog.parks?.remove(at: dogParkIndex)
        }
      // 1. remove the park in the data store, with autosave
      modelContext.delete(parks[index])
    }
})

Parks: Collect Them All

The last thing that app needs at this point is a way to display the dog’s favorite parks. There is a horizontal stack view in the app to do that. Select the ParkStackView from the Project Navigator. Add support for SwiftData. At the top of the file import SwiftData and at the top of the ParkStackView struct add the modelContext.

// at the top of the file
import SwiftData

// at the top of the main struct
@Environment(\.modelContext) var modelContext
var parks: [ParkModel]
#Preview {
  let container = try! ModelContainer(for: DogModel.self)
  let riverdale = ParkModel(name: "Riverdale Park")
  let withrow = ParkModel(name: "Withrow Park")
  let greenwood = ParkModel(name: "Greewood Park")
  let parks = [riverdale, withrow, greenwood]

  return ParkStackView(parks: parks)
    .modelContainer(container)
}
ForEach(parks) { park in
  Text(park.name)
  // ...
}
VStack {
  if let parks = dog.parks {
    ViewThatFits {
      ScrollView(.horizontal, showsIndicators: false) {
        ParkStackView(parks: parks)
      }
    }
  }
}
See forum comments
Cinema mode Download course materials from Github
Previous: One to Many Relationships Demo Next: Sorting, Filtering & Relationships Conclusion