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

Sort Filter 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 first lesson or you can start with the app in the Starter folder for this lesson. There are a few new files that have been added. There is a new BreedModel, ParkModel and the files that you’ll build on to list, create and edit new records. Let’s take a look:

// BreedModel
import Foundation

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

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

// ParkModel
import Foundation

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

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

Sorting the Dogs

To begin, you’ll update the mock data in DogModel by adding a color value and a breed to the dogs. Update the DogModel extension that contains the preview data.

// in the extension DogModel
container.mainContext.insert(
  DogModel(
    name: "Mac",
    age: 11,
    weight: 90,
    color: "Yellow",
    breed: "Labrador Retriever",
    image: nil
  )
)
container.mainContext.insert(
  DogModel(
    name: "Sorcha",
    age: 1,
    weight: 40,
    color: "Yellow",
    breed: "Golden Retriever",
    image: nil
  )
)
container.mainContext.insert(
  DogModel(
    name: "Violet",
    age: 4,
    weight: 85,
    color: "Gray",
    breed: "Bouvier",
    image: nil
  )
)
container.mainContext.insert(
  DogModel(
    name: "Kirby",
    age: 10,
    weight: 95,
    color: "Fox Red",
    breed: "Labrador Retriever",
    image: nil
  )
)
container.mainContext.insert(
  DogModel(
    name: "Priscilla",
    age: 17,
    weight: 65,
    color: "White",
    breed: "Mixed",
    image: nil
  )
)
import SwiftUI
import SwiftData

struct DogList: View {
  @Environment(\.modelContext) private var modelContext
  @Query private var dogs: [DogModel]

  var body: some View {
    Text("Hello, World!")
  }
}

#Preview {
  DogList()
}
List {
  ForEach(dogs) { dog in
    NavigationLink {
      EditDogView(dog: dog)
    } label: {
      HStack {
        Image(systemName: "dog")
          .imageScale(.large)
          .foregroundStyle(.tint)
        Text(dog.name)
      }
    }
  }
  .onDelete(perform: dogToDelete)
}
NavigationStack {
  DogList() // add the DogList here
.navigationTitle("Good Dogs")
#Preview {
  DogList()
    .modelContainer(DogModel.preview)
}

Random Access

You might recall that SwiftData does not fetch the data in any order. You might also wonder if you need the @Query in the DogListView. Of course you don’t since the list of dogs is now coming from the DogList. Check this out, if you comment out the line with @Query in the DogListView and watch the preview, the dogs seem to change position. Try commenting and uncommenting and you’ll see that the dogs are listed in a random order. You might have an application for random dogs, but sorting them makes more sense in most cases.

Sort and Order

In the DogList file add sort with a keypath for the dog’s name, like the following.

@Query(sort: \DogModel.name) private var dogs: [DogModel]
@Query(sort: \DogModel.name, order: .reverse) private var dogs: [DogModel]
// for example - defining model type in Query
@Query<DogModel>(sort: [
  SortDescriptor(\.name, order: .reverse)
  ])
  private var dogs: [DogModel]

SortDescriptors For Multi-Sort

Suppose you have a lot of dogs and you want to sort them with a few factors. For more complicated sorting you make use of an array of SortDescriptors.

@Query(sort: [
  SortDescriptor(\DogModel.age, order: .reverse)
]) private var dogs: [DogModel]
Text("age: \(String(describing: dog.age ?? 0))")
  .font(.footnote)
VStack(alignment: .leading) {
  Text(dog.name)
    .font(.title2)
  Text("age: \(String(describing: dog.age ?? 0))")
    .font(.footnote)
}
@Query(sort: [
  SortDescriptor(\DogModel.age, order: .reverse),
  SortDescriptor(\DogModel.name)
]) private var dogs: [DogModel]

Sort Menu

It would be convenient to add a menu item to change the sorting. Create an enum for sort order. In the Project Navigator create a new Group named Enumerations. Add a new Swift file called SortOrder in the group. Add a String enum adopting Identifiable and CaseIterable, with age and name cases.

enum SortOrder: String, Identifiable, CaseIterable {
  case name, age
  var id: Self {
    self
  }
}
@Query private var dogs: [DogModel]

init(sortOrder: SortOrder) {
  let sortDescriptors: [SortDescriptor<DogModel>] = switch sortOrder {
  case .name:
    [SortDescriptor(\DogModel.name)]
  case .age:
    [SortDescriptor(\DogModel.age)]
  }
  _dogs = Query(sort: sortDescriptors)
}
#Preview {
  DogList(sortOrder: .name)
    .modelContainer(DogModel.preview)
}
@State private var sortOrder = SortOrder.name
DogList(sortOrder: sortOrder)
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)
  }
}
case .age:
  [SortDescriptor(\DogModel.age),
    SortDescriptor(\DogModel.name)]
init(sortOrder: SortOrder) {
  let sortDescriptors: [SortDescriptor<DogModel>] = switch sortOrder {
  case .name:
    [SortDescriptor(\DogModel.name)]
  case .age:
    [SortDescriptor(\DogModel.age),
      SortDescriptor(\DogModel.name)]
  }
  _dogs = Query(sort: sortDescriptors)
}

Finding the Good Dog With Filter

Sorting organizes data into familiar patterns, but narrowing down large sets of data or finding specific records adds power to you apps. For clarity, start by commenting out the line were the query is assigned in the sort order initializer. It will interfere with the filter until we fully incorporate it.

//_dogs = Query(sort: sortDescriptors)
@Query(filter: #Predicate<DogModel> { dog in
  dog.breed == "Labrador Retriever"
}) private var dogs: [DogModel]
init(sortOrder: SortOrder, filterString: String) {
  let sortDescriptors: [SortDescriptor<DogModel>] = switch sortOrder {
  case .name:
    [SortDescriptor(\DogModel.name)]
  case .age:
    [SortDescriptor(\DogModel.age),
      SortDescriptor(\DogModel.name)]
  }
  let predicate = #Predicate<DogModel> { dog in
    dog.breed?.localizedStandardContains(filterString) ?? false
    || dog.name.localizedStandardContains(filterString)
    || filterString.isEmpty
  }
  _dogs = Query(filter: predicate, sort: sortDescriptors)
}
DogList(sortOrder: sortOrder, filterString: filter)
  .searchable(text: $filter, prompt: Text("Filter on name or breed"))

Empty Dog Park?

You’ve added mock data to help with testing your views, and now you can sort and filter the records. However, what happens when there are no results? Apple added ContentUnavailableView to handle this case, where you can populate an empty view with helpful information. In fact, this is also a good thing to add when your app is new and no records have been created.

@State private var message = ""
@State private var dogCount = 0
Group {
  List {
    ForEach(dogs) { dog in
      NavigationLink {
        EditDogView(dog: dog)
      } label: {
        HStack {
          Image(systemName: "dog")
            .imageScale(.large)
            .foregroundStyle(.tint)
          VStack(alignment: .leading) {
            Text(dog.name)
              .font(.title2)
            Text("age: \(String(describing: dog.age ?? 0))")
              .font(.footnote)
          }
        }
      }
    }
    .onDelete(perform: dogToDelete)
  }
}

.onAppear() {
  dogCount = dogs.count
  if dogCount == 0 {
    message = "Enter a dog."
  } else {
    message = "No dogs found."
  }
}
if !dogs.isEmpty {
  // the List will go here
} else {
  // empty view will go here
}
if !dogs.isEmpty {
  // the List code is here
} else {
  ContentUnavailableView(
    message,
    systemImage: "dog"
  )
}
See forum comments
Cinema mode Download course materials from Github
Previous: Managing Fetched Data Next: One to Many Relationships Demo