Greetings, traveler!
Even though SwiftData is based on CoreData, it is relatively young and does not offer some convenient tools that we can expect from it. This article will cover a case where we must dynamically change the predicate based on a state property value.
FetchDescriptor
Consider this example. We have an item list that we display on the screen. Every element is stored in SwiftData and looks like this.
@Model
final class Item {
@Attribute(.unique)
let id: UUID
let title: String
let date: Date
init(
id: UUID,
title: String,
date: Date
) {
self.id = id
self.title = title
self.date = date
}
}
Our Item model has a date variable. We want to limit the number of displayed items via FetchDescriptor
. Let’s create an example, then.
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Query(
FetchDescriptor<Item>(
predicate: #Predicate<Item> { $0.date >= .now },
sortBy: [SortDescriptor(\.date)]
)
) var items: [Item]
var body: some View {
List {
ForEach(items) {
Text($0.title)
}
}
}
}
Everything seems to be okay. But what if we want to make our Query
dynamic and base it on specific @State
properties? We cannot do it for now.
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var startDate: Date = .distantPast
@State private var endDate: Date = .distantFuture
@Query(
FetchDescriptor<Item>(
predicate: #Predicate<Item> { $0.date >= startDate && $0.date <= endDate },
sortBy: [SortDescriptor(\Item.title)]
)
) var items: [Item]
var body: some View {
List {
ForEach(items) {
Text($0.title)
}
}
}
}
❌ Instance member ‘endDate’ cannot be used on type ‘ContentView’; did you mean to use a value of this type instead?
❌ Instance member ‘startDate’ cannot be used on type ‘ContentView’; did you mean to use a value of this type instead?
DynamicQueryView
Since SwiftData does not offer a tool to solve this problem automatically, we should figure it out ourselves. To make it work, we must inject the date values into the View initializer. We can do this right in the parent View.
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Query var items: [Item]
init(startDate: Date, endDate: Date) {
let predicate = #Predicate<Item> {
$0.date >= startDate && $0.date <= endDate
}
_items = Query(filter: predicate, sort: \.date)
}
var body: some View {
List {
ForEach(items) {
Text($0.title)
}
}
}
}
If we want to apply changes dynamically and not worry about such injection, we can create a generic wrapper View. This view will allow us to use any Element
that is a PersistentModel
and produce any View
as its content. We will inject the descriptor via its constructor.
struct DynamicQueryView<Element: PersistentModel, Content: View>: View {
private let descriptor: FetchDescriptor<Element>
private let content: ([Element]) -> Content
@Query private var items: [Element]
init(
_ descriptor: FetchDescriptor<Element>,
@ViewBuilder content: @escaping ([Element]) -> Content
) {
self.descriptor = descriptor
self.content = content
_items = Query(descriptor)
}
var body: some View {
content(items)
}
}
In our parent View, we should create a computed property to produce a descriptor. We can use our state properties as well.
private var descriptor: FetchDescriptor<Item> {
let predicate = #Predicate<Item> {
$0.date >= startDate && $0.date <= endDate
}
return FetchDescriptor<Item>(
predicate: predicate,
sortBy: [SortDescriptor(\.date)]
)
}
Now, we can use our DynamicQueryView
to wrap the ForEach
View.
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var startDate: Date = .distantPast
@State private var endDate: Date = .distantFuture
private var descriptor: FetchDescriptor<Item> {
let predicate = #Predicate<Item> {
$0.date >= startDate && $0.date <= endDate
}
return FetchDescriptor<Item>(
predicate: predicate,
sortBy: [SortDescriptor(\.date)]
)
}
var body: some View {
List {
DynamicQueryView(descriptor) { items in
ForEach(items) {
Text($0.title)
}
}
}
}
}
And that’s it! Everything is working as expected now.
Conclusion
I hope Apple will continue developing SwiftData, but for now, we need some workarounds to make it work in specific cases.
If you enjoyed this article, please feel free to follow me on my social media: