Data Persistence with SwiftData

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

Lesson 03: SwiftData Techniques

SwiftData Techniques 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

You can start with the app you started building in the previous lessons, or you can start with the app in the Starter folder for this lesson. In Xcode, select DogListView from the Project Navigator. In the Canvas preview, switch the Preview Device to an iPad, and wait for the preview to update. Tap Zoom to Fit on the right. Tap the Device Settings on the left toggle on the Orientation, and select Landscape Left.

@State private var selectedDog: DogModel?
NavigationSplitView {
  DogList(sortOrder: sortOrder, filterString: filter)
  // ...
  // end of ToolbarItem "Sort"
} detail: {
  // add NavigationLink here
}
  // ...
} detail: {
  if let selectedDog {
    NavigationLink(value: selectedDog) {
      EditDogView(dog: selectedDog)
    }
  } else {
    Text("Select a dog!")
  }
}
var body: some View {
  NavigationSplitView {
    DogList(sortOrder: sortOrder, filterString: filter)
      .searchable(text: $filter, prompt: Text("Filter on name or breed"))
      .navigationTitle("Good Dogs")
      .toolbar {
        ToolbarItem(placement: .primaryAction) {
          Button("Add New Dog", systemImage: "plus") {
            showingNewDogScreen = true
          }
        }
      }
      .sheet(isPresented: $showingNewDogScreen) {
        NewDogView()
          .presentationDetents([.medium, .large])
      }
      .toolbar {
        ToolbarItem {
          Menu("Sort", systemImage: "arrow.up.arrow.down") {
            Picker("Sort Dogs", selection: $sortOrder) {
              ForEach(SortOrder.allCases) { sortOrder in
                Text("Sort By: \(String(describing: sortOrder))").tag(sortOrder)
              }
            }
            .buttonStyle(.bordered)
            .pickerStyle(.inline)
          }
        }
      }
  } detail: {
    if selectedDog != nil {
      NavigationLink(value: selectedDog) {
        EditDogView(dog: selectedDog!)
      }
    } else {
      Text("Select a dog!")
    }
  }
}
.onChange(of: dog) {
  name = dog.name
  age = dog.age ?? 0
  weight = dog.weight ?? 0
  color = dog.color ?? ""
  image = dog.image
}
NavigationSplitView(columnVisibility: .constant(.doubleColumn)) {
  // ...
} detail: {
  //...
}
.navigationSplitViewStyle(.balanced)
.frame(minWidth: 250)

Adding Unique Attributes

Currently, people can create any number of dogs in the app, and they can choose an existing breed from the picker. However, nothing is preventing them from entering the same breed name over and over. This isn’t a huge issue with a few dogs, but with a large enough data set, it could add up to a lot of unnecessary storage. It would also populate the picker with duplicate names. There are also cases where you’d want to store a truly unique value, like a car’s VIN number, or a dog’s city license number.

@Attribute(.unique) var name: String

Adding an Unknown Breed

Now that you’ve looked at having unique entries think about what you could do about unknown values. Using empty strings is fine, but there might come a day when you need to find and sort every dog. People can always enter Unknown in by hand. But you’re now becoming a SwiftData expert, and you can do better. People don’t like looking at an empty app. Get them started with a dog.

var container: ModelContainer {
  return container
}
@MainActor
var container: ModelContainer {
  let schema = Schema([DogModel.self])
  let container = try! ModelContainer(for: schema)

  // make a dog here

  return container
}
var dogFetchDescriptor = FetchDescriptor<DogModel>()
dogFetchDescriptor.fetchLimit = 1
guard try! container.mainContext.fetch(dogFetchDescriptor).count == 0 else { return container }
let dogs = [
  DogModel(
    name: "Rover",
    breed: BreedModel(name: "Unknown Breed"))
]
@MainActor
var container: ModelContainer {
  let schema = Schema([DogModel.self])
  let container = try! ModelContainer(for: schema)

  // check that there are no dogs in the store
  var dogFetchDescriptor = FetchDescriptor<DogModel>()
  dogFetchDescriptor.fetchLimit = 1
  guard try! container.mainContext.fetch(dogFetchDescriptor).count == 0 else { return container }

  let dogs = [
    DogModel(
      name: "Rover",
      breed: BreedModel(name: "Unknown Breed"))
  ]

  for dog in dogs {
    container.mainContext.insert(dog)
  }

  return container
}
WindowGroup {
  DogListView()
    .modelContainer(container)
}

Adding a Query.

Now that you have this Unknown Breed available, there’s no reason to create a dog with an empty breed name. With SwiftData, you can use multiple queries. Currently, the NewDogView doesn’t have its own @Query. As a subview of DogList, it inherits that DogModel query when it inserts a new dog. You will need to check if there’s an Unknown Breed still available and if the user hasn’t deleted it. This can be done with a #Predicate on the breed name in the BreedModel. Then, you can either use found breed or reinsert it if needed.

// at the top of the file
import SwiftData

// at the top of the NewDogView struct add:
@Query(filter: #Predicate<BreedModel> { breed in
    breed.name == "Unknown Breed"
  }) private var breeds: [BreedModel]
let breed: BreedModel
if breeds.isEmpty {
  // make a new breed
  breed = BreedModel(name: "Unknown Breed")
} else {
  // found at least one
  breed = breeds[0]
}
let newDog = DogModel(
  name: name,
  breed: breed)
modelContext.insert(newDog)

Implementing Undo - Doggy Spin

Up to this point, users can create and update dogs. They can edit and delete dogs, as well. To add even more polish to your app, you’ll implement undo. The UndoManager is built into SwiftData, but as mentioned, you need to enable it to use it. The ability to undo and redo can be computationally expensive and consume memory, so it’s not enabled by default. Once again, you use the mainContext to access the UndoManager(). You can create a modelContainer and then add undo on the container. Alternatively, you can add it to your modelContainer, where you created the container if you’re not using any customization. Since you just added a custom container, you’ll use the first method.

container.mainContext.undoManager = UndoManager()
@MainActor
var container: ModelContainer {
  do {
    let schema = Schema([DogModel.self])
    let container = try! ModelContainer(for: schema)
    //container.mainContext.autosaveEnabled = false
    // here's the undo
    container.mainContext.undoManager = UndoManager()

    // check that there are no dogs in the store
    var dogFetchDescriptor = FetchDescriptor<DogModel>()
    dogFetchDescriptor.fetchLimit = 1
    guard try container.mainContext.fetch(dogFetchDescriptor).count == 0 else { return container }

    let dogs = [
    DogModel(
      name: "Rover",
      breed: BreedModel(name: "Unknown Breed")
    )
  ]

    for dog in dogs {
      container.mainContext.insert(dog)
    }

    return container
  } catch {
    fatalError("Failed to create container")
  }
}
@Environment(\.undoManager) private var undoManager
.toolbar {
  ToolbarItem {
    Button("Undo", systemImage: "arrow.uturn.left") {
      withAnimation {
        modelContext.undoManager?.undo()
      }
    }
    .disabled(modelContext.undoManager?.canUndo == false)
  }
}
container.mainContext.undoManger?.levelsOfUndo = 2

GoodDogs Schema

One last thing you’ll do in this lesson is to create a custom schema. In order to make a custom schema, you’ll add a ModelConfiguration. In the GoodDogsApp, add the following before creating the container:

let config = ModelConfiguration("GoodDogs", schema: schema)
let container = try! ModelContainer(for: schema, configurations: config)
GoodDogs.store
GoodDogs.store-shm
GoodDags.store-wal

See forum comments
Cinema mode Download course materials from Github
Previous: SwiftData Techniques Instruction Next: SwiftData Techniques Conclusion