Skip to main content

Filtering the results from a SwiftData query

About 2 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftswiftdataxcodeappstore

Filtering the results from a SwiftData query 관련

SwiftData by Example

Back to Home

Filtering the results from a SwiftData query | SwiftData by Example

Filtering the results from a SwiftData query

Updated for Xcode 15

Filtering in SwiftData is done with predicates: a test that can be applied to decide whether objects should appear in the resulting array or not. This is done with a special #Predicate macro that takes Swift code we write and converts it into filters the underlying database can understand.

Tips

If you used Core Data before, #Predicate is similar to NSPredicate. The main difference is that #Predicate gets typechecked at compile time, but there is no NSCompoundPredicate equivalent at this time.

Let's try out a few sample predicates. For example, we might say that our app shouldn’t show any destinations with a low priority:

init(sort: SortDescriptor<Destination>) {
    _destinations = Query(filter: #Predicate {
        $0.priority >= 2
    }, sort: [sort])
}

As you can see, we give #Predicate a closure that takes one object from the query and applies a test to it. In this case, does the object have a priority of at least 2?

Or we could write a predicate that only shows destinations that are upcoming in our trip, ignoring those that are older than now. We can't read Date.now inside the #Predicate macro, but if we take a local copy of it first then it will work fine. So, our predicate would be this:

init(sort: SortDescriptor<Destination>) {
    let now = Date.now

    _destinations = Query(filter: #Predicate {
        $0.date > now
    }, sort: [sort])
}

Those are both interesting, but in this project we're going to use a predicate to let the user search for specific destinations using SwiftUI's searchable() modifier.

As this will change while the app is running, specifying a dynamic filter is just the same as specifying a dynamic sort order: we need to create state in ContentView, then pass it into the initializer for DestinationListingView.

This takes four steps, starting with adding some new state in ContentView to store whatever is the user's current search text:

@State private var searchText = ""

We then need to bind that to a searchable() modifier, so put this next to navigationTitle() in ContentView:

.searchable(text: $searchText)

Third, we need to update the initializer of DestinationListingView to accept a search string, and use it for the query predicate:

init(sort: SortDescriptor<Destination>, searchString: String) {
    _destinations = Query(filter: #Predicate {
        if searchString.isEmpty {
            return true
        } else {
            return $0.name.localizedStandardContains(searchString)
        }
    }, sort: [sort])
}

Note

localizedStandardContains() is almost always the best way to do user-facing string searches. If you use the regular contains() method you'll get case-sensitive searching.

Make sure you send a sample search into your preview, even if that's just an empty string:

DestinationListingView(sort: SortDescriptor(\Destination.name), searchString: "")

Finally, we need to edit the way DestinationListingView is created in ContentView, so that it passes in both the current sort order and search string:

DestinationListingView(sort: sortOrder, searchString: searchText)

And now we have dynamic filtering – nice!


이찬희 (MarkiiimarK)
Never Stop Learning.