How to create a custom FetchDescriptor
How to create a custom FetchDescriptor êŽë š
Updated for Xcode 15
SwiftDataâs FetchDescriptor
struct is similar to Core Dataâs NSFetchRequest
, allowing us to fetch a particular set of objects by specifying a model type, predicate, and sort order. However, it also gives us precise control over whatâs fetched: you can set a limit of objects to fetch, prefetch relationships, and more.
In its simplest case, you issue a custom fetch request like this:
let fetchDescriptor = FetchDescriptor<Movie>()
do {
let movies = try modelContext.fetch(fetchDescriptor)
for movie in movies {
print("Found \(movie.name)")
}
} catch {
print("Failed to load Movie model.")
}
That loads all instances of a Movie
model, with no filtering or sorting applied. It wonât load any relationships automatically, and will instead load those only when you request them.
Important
If you issue a custom fetch immediately after inserting some data, any data linked through relationships wonât be visible even if youâve manually called save()
, and even if you specifically set includePendingChanges
to true. Yes, when you call save()
the data is written to disk immediately, but SwiftData seems to wait until the next runloop before making that data available for querying.
You can customize your fetch descriptor in all sorts of ways, and I want to show some code samples for each.
First, you can specify a custom sort order using one or more SortDescriptor
objects in an array:
let fetchDescriptor = FetchDescriptor<Movie>(sortBy: [SortDescriptor(\.name), SortDescriptor(\.releaseDate, order: .reverse)])
Second, you can filter your results using the #Predicate
macro, so we could look for unreleased movies like this:
let now = Date.now
let unreleasedMovies = FetchDescriptor<Movie>(predicate: #Predicate { movie in
movie.releaseDate > now
})
Note
In that sample we need to copy Date.now
into a local value so SwiftData can turn it into a filter.
Third, you can limit the number of objects you want to read by creating your fetch descriptor as a variable then setting its fetchLimit
property. This accepts an optional integer, where either nil
or 0 both mean âfetch all objectsâ.
For example, we could ask for the three newest movies like this:
var fetchDescriptor = FetchDescriptor<Movie>(sortBy: [SortDescriptor(\.releaseDate, order: .reverse)])
fetchDescriptor.fetchLimit = 3
This works great in combination with the offset
parameter of fetch descriptors, which allow us to do paging â we can tell SwiftData to skip the first n results. This is helpful when you know you have many results, so rather than fetching everything at once you can instead fetch in pages of 50 or 100 at a time.
For example, if we had a page size of 100 and we were currently on the third page (counting from 0), weâd write code like this:
let pageSize = 100
let pageNumber = 2
var fetchDescriptor = FetchDescriptor<Movie>(sortBy: [SortDescriptor(\.releaseDate, order: .reverse)])
fetchDescriptor.fetchOffset = pageNumber * pageSize
fetchDescriptor.fetchLimit = pageSize
That will set fetchOffset
to 200 and fetchLimit
to 100, meaning that SwiftData will attempt to return objects 201 to 300 in the results.
A fourth way to customize your fetch descriptor is by listing the properties you want to fetch. This is helpful for times when your objects are large: rather than loading the entire object into memory at once, you can instead request just the properties you intend to use.
For example, if you knew you wanted to show just the release dates and names of the latest movies, you could use this:
var fetchDescriptor = FetchDescriptor<Movie>(sortBy: [SortDescriptor(\.releaseDate, order: .reverse)])
fetchDescriptor.propertiesToFetch = [\.name, \.releaseDate]
Note
If you donât include a property in propertiesToFetch
then later use it, SwiftData will automatically fetch the data at the point of use. This uses the same system of faulting that Core Data used â the properties you donât request are filled with placeholders that automatically get substituted with their real data on request.
Another great performance optimization you can make is to set the relationshipKeyPathsForPrefetching
property to an array of relationship properties you want to prefetch. This is empty by default because SwiftData doesnât fetch relationships until they are used, but if you know youâll use that data then prefetching allows SwiftData to batch request it all for more efficiently.
Continuing our movie example, we might be building a screen that shows the directors behind the most recent movies. If the director name were baked right into the Movie
model then it would be fetched automatically unless we specifically requested otherwise, but if it were a relationship then we could prefetch it like this:
var fetchDescriptor = FetchDescriptor<Movie>(sortBy: [SortDescriptor(\.releaseDate, order: .forward)])
fetchDescriptor.relationshipKeyPathsForPrefetching = [\.director]
The last customization point is includePendingChanges
, which controls whether the fetch should include changes youâve made that have yet to be saved. This defaults to true, and while there were one or two places you would want otherwise with Core Data I genuinely canât see this being useful in SwiftData â Iâd leave it alone, if I were you.