SwiftData: Dynamically Query Filtering
The SwiftUI ‘searchable’ modifier
As described in my previous article SwiftUI: Dynamically Filtering FetchRequest from IOS 15 SwiftUI has been enriched by an useful modifier searchable that allow to achieve, in pretty straightforward way a full working search bar.
I alredy dealt with problem with @FetchRequest
dynamic filtering and I’ve created a DynamicFetchRequestView
to solve this problem, but now I’m involved in porting CoreData based application to new SwiftData approach and I’ve refactor the CoreData based DynamicFetchRequestView
to DynamicQuerytView
that allow to achieve dynamic filtering also using new SwiftData @Query
property wrapper.
Problem with ‘@Query’ dynamic filtering
The issue (the same with @FetchRequest
) is that we need to assign the filter when we declare the variable for our query results, as shown in the example below:
import SwiftData
import SwiftUI
// PersistentModel class
@Model final public class Snippet {
let dateDate: Date
var title: String
var code: String
}
// SwiftUI View that Query snippets by title = "test"
struct ContentView: View {
@Query(filter: #Predicate<Snippet> { $0.title.contains("test") },
sort: [SortDescriptor(\Snippet.date)] )
var snippets: [Snippet]
var body: some View {
List {
ForEach(snippets) { snippet in
SnippetRow(snippet: snippet)
}
}
}
}
As you can see the filter is set when we declare the snippets
variable that will contains the query results, so if we want to change the filter the only way is to redeclare the snippets
variable itself.
Solution implementing ‘DynamicQueryView’
Since behind the @Query
property wrapper there is a Query Object we can use it to achieve dynamic filtering.
The following implementation of DynamicQueryView
use initializer to explictly instantiate the Query Object providing filter and/or sort descriptors.
import SwiftUI
import SwiftData
public struct DynamicQueryView<T: PersistentModel, Content: View>: View {
// declare Query macro
@Query var query: [T]
// this is our content closure; we'll call this once for each item in the list
let content: ( [T] ) -> Content
public var body: some View {
self.content(query)
}
init( descriptor: FetchDescriptor<T>, @ViewBuilder content: @escaping ( [T] ) -> Content) {
// initialize query object with provided arguments
_query = Query( descriptor )
self.content = content
}
}
As you can see the View is pretty simple, as said the trick is inside initializer where we are able to instantiate a Query Object providing the required arguments assigning it to the property wrapper throught the notation _query
. After that, the request will be automatically performed by the View when its render is required and the result will be passed to the custom content that is a @ViewBuilder
provided in initializer itself.
Bonus: customize ‘DynamicQueryView’ by PersistenModel
Now we can use the powerful of Swift extension tecnique, to add a convenience initializer for each Entity on which we want have a dynamic filtering.
// Add Initializer for 'Snippet' filtering
extension DynamicQueryView where T : Snippet { // 👀
init( filterByTitle searchTitle: String, @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {
let filter = #Predicate<T> { $0.title.contains(searchTitle) }
let sort = [SortDescriptor(\Snippet.date)]
self.init( FetchDescriptor( predicate: filter, sortBy: sort) )
}
}
Put ‘searchable’ modifier and ‘DynamicQueryView’ together
We are ready to translate the previous example using the new DynamicQueryView
to achieve a dynamic filtering by title.
struct ContentView: View {
@State var searchTitle:String?
var body: some View {
DynamicQueryView( filterByTitle: searchTitle ) { snipptes in
List {
ForEach(snippets) { snippet in
SnippetRow(snippet: snippet)
}
}
}
.searchable( text: $searchTitle )
}
}
Conclusion
In this article I shared my hands-on experience in moving from CoreData to new [SwifData]. SwiftData makes it easy to persist data using declarative code and it’s designed to integrate seamlessly with SwiftUI.
Hope this help, in the meanwhile happy coding and … enjoy SwiftData! 👋