How to dynamically change a query's sort order or predicate
How to dynamically change a query's sort order or predicate 관련
Updated for Xcode 15
When it comes to sorting our data, SwiftData has two approaches: the trivial version that works great in a WWDC video and a handful of small projects, and a more complex version that is much more indicative of the kinds of apps you’ll be building in real life.
We’ve already seen the simple version, because it’s where we can put our sort order directly into the @Query
macro, like this:
@Query(sort: \.name, order: .reverse) var users: [User]
In practice, however, that doesn’t happen much – usually users want to be able to set the sort order dynamically, which is not actually supported by @Query
right now.
To get dynamic sorting working you need to move your @Query
properties down a view in SwiftUI’s hierarchy – you need to put it into a subview where you can provide a sort value using dependency injection.
This means making a new SwiftUI that uses @Query
to show the SwiftData objects you're working with, then embed that in a parent view that provides some UI for the user to select their sort order or filter.
For example, if we were working with a User
model then we might create a UserListingView
like this one:
import SwiftData
import SwiftUI
struct UserListingView: View {
@Query var users: [User]
@Environment(\.modelContext) var modelContext
var body: some View {
List {
ForEach(users) { user in
NavigationLink(value: user) {
Text("\(user.name) is \(user.age) years old")
}
}
.onDelete(perform: deleteUser)
}
}
func deleteUser(_ indexSet: IndexSet) {
for item in indexSet {
let object = users[item]
modelContext.delete(object)
}
}
}
And then back in ContentView
we could create that inside a list such as this:
NavigationStack {
UserListingView()
}
This change doesn’t actually handle sorting – this is just the setup required to make sorting possible. However, because we now have a subview we’re able to send values into there to control the @Query
property wrapper.
This takes four steps in in total:
- Telling the
UserListingView
that it needs to be created with some kind of sort order. - Making some storage to hold whatever is the currently active sort order when your program is running.
- Passing that value into
UserListingView
when it’s created. - Creating some UI to adjust that sort order based on the user’s settings.
To complete that first step, we need to adapt the initializer in UserListingView
so that it changes the query using a sort descriptor passed in from a parent view. This needs to so change the query object itself rather than the array inside it, so as a result we need to access the underscored property name like this:
init(sort: SortDescriptor<User>) {
_users = Query(sort: [sort])
}
Then in ContentView
we would add a property to store the current sort order with a sensible default:
@State private var sortOrder = SortDescriptor(\User.name)
We can then use pass that into UserListingView
wherever it's embedded, like so:
UserListingView(sort: sortOrder)
And finally, we need some UI in ContentView
to present the user with various sorting options, then adjust our sort order as appropriate. For example, we could put this into a toolbar:
Menu("Sort") {
Picker("Sort", selection: $sortOrder) {
Text("Name")
.tag(SortDescriptor(\User.name))
Text("Age")
.tag(SortDescriptor(\User.age, order: .reverse))
Text("City")
.tag(SortDescriptor(\User.city))
}
.pickerStyle(.inline)
}
What's happening here is that we're moving the sort selection up one level from UserListingView
, which means we can now control it dynamically. SwiftUI will automatically recreate UserListingView
whenever that sort order changes, which in turn will recreate the query.
Tips
You could easily adjust this so your child view's initializer accepts an array of sort descriptors.