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
}
}
These models are not yet set up for SwiftData. You’ll update them soon.
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.
Pro Tip: if you select a long list of properties you can press Control-M to expand and put each value pair on it’s own line, making it easier to read and reason about.
Add some breed and color preview values to the DogModel’s extension.
// 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
)
)
Also move the dogs list from DogListView to it’s own view. Create a new SwiftUI view file named DogList. At the top import SwiftData. Inside the struct add an @Environment(\.modelContext) as you did in the previous lesson. While you are there add the @Query to fetch the dogs.
Your file will look like this,
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()
}
Pro Tip: You are going to be adding the modelContext many times during the course. You might look at creating a Code Snippet. In your code, select the code
@Environment(\.modelContext) private var modelContext. The Right-Click and choose Create Code Snippet. In the dialog, give it a meaningful name like modelContext and give it a Completion likemc~. Later when you start to typemcthe code completion will offer your snippet.
In the DogListView, select the List, including the .onDelete call. Cut it and paste it into the view in DogList, replacing the Text view.
List {
ForEach(dogs) { dog in
NavigationLink {
EditDogView(dog: dog)
} label: {
HStack {
Image(systemName: "dog")
.imageScale(.large)
.foregroundStyle(.tint)
Text(dog.name)
}
}
}
.onDelete(perform: dogToDelete)
}
Next cut the func dogToDelete() from DogListView and paste it into DogList. This will fix the compiler warning. Now you’ll need to add DogList() where you cut the List in DogListView. All of the warnings should be gone now.
NavigationStack {
DogList() // add the DogList here
.navigationTitle("Good Dogs")
Head back to DogList to fix the #Preview. Add the modelContainer with DogModel.preview.
#Preview {
DogList()
.modelContainer(DogModel.preview)
}
The Canvas previews should update and the app should still be able to function with the refresh icon. Press Command-B to build or press the Refresh button to start the 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]
When the preview updates, you’ll see that Kirby is first and Violet is last. The sort: is a quick way to do a basic alphabetical sort. You can also add order: .reverse to sort in reverse order. Try it out.
@Query(sort: \DogModel.name, order: .reverse) private var dogs: [DogModel]
The important thing to notice is that we add DogModel type in the keypath along with the property to sort on. You might recall that you can add the model type in angle brackets on the @Query. Like this for example.
// 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.
Replace the simple sort: by adding a SortDescriptor in an array like this.
@Query(sort: [
SortDescriptor(\DogModel.age, order: .reverse)
]) private var dogs: [DogModel]
Here you are sorting the dogs by age, oldest first. Update the UI to show the age in the list. In the DogList, select the Text(dog.name), open the contextual menu and Embed it in a VStack. You can Control-click to open the contextual menu. Below the dog.name line add the age like so.
Text("age: \(String(describing: dog.age ?? 0))")
.font(.footnote)
Add a font modifier with .title2 and add leading alignment to the VStack.
The VStack should look like this.
VStack(alignment: .leading) {
Text(dog.name)
.font(.title2)
Text("age: \(String(describing: dog.age ?? 0))")
.font(.footnote)
}
Add a second SortDescriptor for name.
@Query(sort: [
SortDescriptor(\DogModel.age, order: .reverse),
SortDescriptor(\DogModel.name)
]) private var dogs: [DogModel]
Now you are sorting by age and then by name. If you like, you can change the dog’s age in the mock data to test this. By the way, if you go back to DogListView you might notice that the sorting no longer uses plain @Query, if you left it there. The data is now sorted in the DogList’s modelContext where SwiftData is doing the work.
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
}
}
Initialize a sortOrder in DogList and move the SortDescriptors to it. The @Query will be simple again.
@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)
}
Now the SortDescriptor can be changed based on the sortOrder case. But first you’ll need to fix the preview by adding a sort order. Use the name case.
#Preview {
DogList(sortOrder: .name)
.modelContainer(DogModel.preview)
}
The DogList() at the call site also needs a sort order value. In DogListView add a @State property for sort order defaulting with .name.
@State private var sortOrder = SortOrder.name
Add the value where DogList() is added.
DogList(sortOrder: sortOrder)
Now you can add a menu style picker inside the toolBar in DogListView. With this menu, you can switch between the sort order cases. Add this inside the current toolbar below the first ToolbarItem
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)
}
}
When the preview updates, try out the menu by tapping the arrows and choosing a case. To reproduce the multi-sort you used earlier, update the .age then .name case in the initializer in DogList.swift inside the sortDescriptors code block, where you have sortOrder.
case .age:
[SortDescriptor(\DogModel.age),
SortDescriptor(\DogModel.name)]
Now when you choose age the dogs sort alphabetically. When you choose age, they sort by age and then by name. The sortOrder initializer will now look like this.
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)
Next add a predicate to filter the data. In the @Query, add filter followed by the #Predicate and DogModel type. In a trailing closure add search on a breed.
@Query(filter: #Predicate<DogModel> { dog in
dog.breed == "Labrador Retriever"
}) private var dogs: [DogModel]
In the sample mock data there are only two Labrador Retrievers. Next you add the predicate to the initializer. Update the initializer by adding in the predicate. Pass in a filterString value. Then add the logic to apply the filterString and add this to the Query assignment.
The initializer will look like the following.
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)
}
The preview will also want an empty filterString value. Also in the DogListView, add a State variable filter with an empty string default value. Now to access and enter a filter string add a .searchable modifier to the DogList() in DogListView. Update it like this.
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.
Start by creating a message to show the users when there are no dogs. Add two State properties at the top of the DogList struct. Add an empty string called message and a dogCount with default value zero.
@State private var message = ""
@State private var dogCount = 0
You’re going to use .onAppear() to check the dogCount amount, but before you do select the List and embed it in a Group. Your code should look like the following.
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)
}
}
You’ll need to have something like this Group to attach the .onAppear() and you’re about the wrap the List in some logic, so it may not be created is there are no dogs.
At the bottom of the Group, after the closing curly brace, add an .onAppear() to check the dogCount. There are two conditions where either there are no dogs found by the filter, or the app is new without any dogs created. Add the following check for dogs.count.
.onAppear() {
dogCount = dogs.count
if dogCount == 0 {
message = "Enter a dog."
} else {
message = "No dogs found."
}
}
Note: You can use .onAppear and .onDisappear to handle states when the view loads and unloads. You can also use the asynchronous
.taskinstead. You’ll learn how to use.tasksoon, but for clarity you’ll use.onAppear()for now.
Next you’ll move the List inside the logic to check the dogCount. Start by adding an if statement at the top of the Group
if !dogs.isEmpty {
// the List will go here
} else {
// empty view will go here
}
Next select the List by double-clicking the opening brace. This highlights the code block. Also Shift-click the List name to include it in your selection. Now cut the List and paste it into the top half of the if statement.
Pro Tip: With one or more lines of code selected you can move the code up with Command-Option-[ keys and move the lines down with Command-Option-], instead of cut & paste.
In the bottom half of the if !dogs.isEmpty check, add a ContentUnavailableView() with your message and a systemImage: "dog".
if !dogs.isEmpty {
// the List code is here
} else {
ContentUnavailableView(
message,
systemImage: "dog"
)
}
Pro Tip: It is often preferable to put the
ContentUnavailableView()in the bottom half of the iflogic. If there is an error in your code, the compiler may complain thatContentUnavailableView()is at fault if it comes first. To find the actual root cause, you would need to comment out theifandContentUnavailableView(). The compiler warning can be confusing. Within theelsecase the compiler is less likely to blame the empty view.
Now that you have the ContentUnavailableView() in place, switch over to DogListView and start the Canvas preview. Enter some text that doesn’t match the mock data, or your stored data if your testing in the Simulator or on a device. You should see No Dogs Found. If you try installing the app with no dogs added, you should see Enter a dog.
That’s all for sorting and filtering with SwiftData. You can take a break and move on to learning about One to Many Relationships in the next demo.