Managing Fetched Data
Good Dogs Learn to Fetch
As you learned in the first lesson, fetching data from the data store with SwiftData and Core Data is quick and straight forward. However, the data doesn’t get returned in a straight forward way. In fact, it’s unsorted. Fortunately the @Query macro supports a sort function that, when you provide a Comparable type as a keypath (like name or date), SwiftData sorts the result by that criteria.
Additionally, you can use an array of SortDescriptor types to sort by multiple properties and set up the direction of the sort. For example you can sort the data by name and then further by another property like the dog ages in order.
A sort query would look like this:
@Query(sort: \DogModel.name) private var dogs: [DogModel]
Notice how the keypath is included with the name of the type DogModel. This is because the compiler can’t infer the type without it. You can specify the type in the @Query to help.
@Query<DogModel>(sort: \.name) private var dogs: [DogModel]
Here you’ve specified the type upfront. When you want to sort with multiple factors you use SortDescriptor.
@Query(sort: [
SortDescriptor(\DogModel.age, order: .reverse),
SortDescriptor(\DogModel.name)
]) private var dogs: [DogModel]
In this example, the dog’s age is used to sort older dogs first and then by name.
Now suppose you don’t want to deal with all the records. You can fetch a subset by using SwiftData’s filter using the new #Predicate macro. The new predicates in SwiftData are fully type checked and are a modern replacement to Core Data’s NSPredicate. The #Predicate macro works with native Swift types and uses Swift Macros in constructing filters.
With a predicate you would create a closure to select the matching records.
@Query(filter: #Predicate<DogModel> { dog in
dog.breed == "Labrador Retriever"
}) private var dogs: [DogModel]
In this example you would tell the predicate the type, DogModel, and then in the closure request only the Labrador Retrievers to be returned. Under the hood, the predicate uses lhs and rhs comparisons. That’s left hand side, and right hand side for each side of the equation. You can use unary, range, comparison, ternary, booleans, types, sequences, and string comparisons in the predicate formula.
Adding in SwiftUI’s @Searchable, you can add a search field to the list. In iOS 17, Searchable got tokenized input to help build structured queries. Then, combining the sort and filter, you can fetch records that match the name or breed of the string you type. With a few lines of code you make it easy for your app’s users to find the information they need.
DogList(sortOrder: sortOrder, filterString: filter)
.searchable(text: $filter, prompt: Text("Filter on name or breed"))
Using the a .searchable() textfield adds a dynamic Search Field to the top of the List.
Puppy Pictures
The app in the Simulator and device can easily support dog images because the Photos framework is integrated. What about the Canvas Previews? UIImage has a pngData property you can use. In the mock data you add UIKit support and place images in the asset catalogs. Then you can add an image to the Canvas preview with a UIImage.
image: UIImage(resource: "myPuppy").pngData()!
Note: Xcode does provide a Preview Assets catalog, however you can have trouble building to a device or the Simulator. Handling that is outside the scope of this course. For now you’ll use the main Assets catalog.
Best of Breed - Relationships
Relationships are links created between model types and are created as reference types in SwiftData. For example, many dogs can share the same breed name and they can visit many of the same dog parks on their daily walks. Using a breed model, you allow the dog owner to create a single breed name and then share that information between several dogs. This saves on the effort of duplication, mitigates input errors, increases query efficiency, and reduces the amount of data to store. Once created,if a breed name is edited the modification updates all the dog records that share the same breed.
One to One
There are three main types of relationships in SwiftData. The first is one-to-one, where there’s a single value on each side. A dog would have one unique dog license. An app user can have one profile, unique to that person.
To create a relationship, you only need to add a reference of each in each model:
// in the DogModel
var license: LicenseModel?
// in the LicenseModel
var dog: DogModel?
You don’t need to mark the relationship with the @Relationship macro, because here the compiler can infer the inverse relationship. You would do this as in the following example, but with SwiftData it’s not required on a basic inverse relationship.
// in the DogModel
@Relationship(inverse: \LicenseModel.license)
var license: LicenseModel?
// in the LicenseModel
@Relationship
var dog: DogModel?
Notice that you only mark it inverse: on one side. Another nice feature of SwiftData is that if the models are in a relationship you only need to list one model on the ModelContainer you set up in the app file.
WindowGroup {
DogListView()
.modelContainer(for: DogModel.self)
}
}
If the LicenseModel was not in the relationship you would have to add both models in the array.
.modelContainer([DogModel.self, LicenseModel.self])
One to Many
The second type of relationship is a one-to-many. This would be the type of relationship where a property can be shared. You could have several dogs that are all of the same breed. In the app you have so far you have repeated the breed as a string. It would be better and less prone to input errors if you had one type of breed. Then assign that breed to the matching dogs.
Many To Many
A many-to-many relationship is the third type. In your city there are several dog parks where you can take your dog. Those parks are shared by multiple owners and their dogs. In other words, many dogs can be related to many parks.
Pick Up After Your Pet - Deleting Records
Cleaning up the data with the delete function is simple with one model. Adding other models in relationships complicates the deletion. By default, SwiftData uses Nullify as the delete rule. If you delete a dog, the breed can be left behind. However, a dog can have a license in a Permits model. You might wonder what happens when the dog record is deleted but it’s license is kept in the data store. It would become an orphaned license record. In that case, you can set the deletion rule to Cascade. When one side of a related object is deleted SwiftData will also delete the record in the other model.
// in the DogModel
@Relationship(deleteRule: .cascade)
var license: LicenseModel?
When the dog gets deleted, the deletion is cascaded, and the related license is also deleted.
If, for some reason, the data shouldn’t get deleted you could use the .deny delete rule. For instance for an order number to prevent reuse.
// in the DogModel
@Relationship(deleteRule: .deny)
var order: OrderModel?
Macro Polo - More on Macros
You learned a bit about @Attribute in the first lesson. There you used the externalStorage option for handling potentially large images.
@Attribute(.externalStorage) var image: Data?
The Attribute macro can also set a value as .unique in your model, where only one instance of a record is needed.
@Attribute(.unique) var country: String
It’s unlikely that more than one country would exist with the same name, for example. SwiftData would allow the value to be inserted once and, if it required alteration later, it would update or upsert the record.
You could decide later to change the names of your properties. That would break the data you had previously stored and potentially crash the app. You can use the .originalName attribute to solve that.
@Attribute(.originalName: "name") var betterName: String
Here the compiler would accept “betterName” for the previously used “name”. The @Relationship also supports .originalName as well.
For unusual or unsupported data types you can use .transformable as attribute. This came over from Core Data as well.
@Attribute(.transformable) var strings: [[String]]
In the example, you’d be storing an array of strings, which is not directly supported. In Core Data, image data and other binaries could be stored as transformable.
Sometimes you might have data that you don’t want to persist and you don’t want to update the UI. It could be a counter value that’s only needed during a session. In that case you would use the @Transient macro.
@Transient var numberOfWordsRead: Int = 0
Now you’ve got a sense of what can be done with the data objects. Move on to the demo and learn how to put this knowledge into practice.