I got Orgtre's answer to the Stackoverflow: Combining Predicate in SwiftData to work (eventually).
You break your desired compound predicate into its simplest logic components.
Then write a simple query like you normally do for SwiftData for each of the single predicates you want to compound, eg.
_books = Query(filter: #Predicate<Book> { $0.title.contains("the")}).
_books = Query(filter: #Predicate<Book> { $0.title.contains("tom")}). //etc
Then expand each one of these predicate's macros by right clicking the word "Predicate", e.g.
Foundation.Predicate<Book>({
PredicateExpressions.build_contains(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.title
),
PredicateExpressions.build_Arg("the")
)
})
Copy just the Predicate Expressions code the macro generates (not the Foundation.Predicate<Book>
bit) and throw away the Query you used to generate the macro. Use the copied code to create a Predicate Expression ready for use inside Orgtre's makePredicate()
function - you will need to swap the $0
for book
or whatever variable you are using in your closure.
let expression2 = PredicateExpressions.build_contains(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(book),
keyPath: \.title
),
PredicateExpressions.build_Arg("the")
)
Rinse and repeat for all the predicates you may need.
For a particular filtering task, append all the predicate expressions you need into an array. Orgtre's function then uses that array (called conditions
in his example)to return a predicate (that is really a compound predicate) you can then use for the filter for your query as per normal SwiftData.
Orgtre's booleans (useExpression1
) just toggle whether or not to append a particular predicate into the compound predicate at any given time that could be controlled by toggles in your UI.
My brain hurts after that paragraph so here is some code to sketch out the above paragraph that hopefully will make things less muddy.
import SwiftUI
import SwiftData
@Model
class Book {
var title: String
var lastReadDate: Date
init(title: String = "", lastReadDate: Date = Date()) {
self.title = title
self.lastReadDate = lastReadDate
}
}
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Query var books: [Book]
init(){
let combinedPredicate = makePredicate(useExpression1: true, useExpression2: true)
_books = Query (filter: combinedPredicate)
}
var body: some View {
VStack {
Button("Add samples"){
addSamples()
}
Text("Hi \(books.count)")
List{
ForEach(books){book in
HStack{
Text("\(book.title)")
Text("\(book.lastReadDate.ISO8601Format())")
}
}
}
}
.padding()
.toolbar {
Button("Add Samples", action: addSamples)
}
}
func addSamples() {
let book1 = Book(title: "the importance of being earnest tom", lastReadDate: Date())
let book2 = Book(title: "the tom swift", lastReadDate: Date())
let book3 = Book(title: "around the world in 80 days", lastReadDate: Date())
modelContext.insert(book1)
modelContext.insert(book2)
modelContext.insert(book3)
}
func makePredicate(useExpression1: Bool, useExpression2: Bool) -> Predicate<Book> {
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
}
return Predicate<Book>({ book in
var conditions: [any StandardPredicateExpression<Bool>] = []
let expression1 =
PredicateExpressions.build_contains(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(book),
keyPath: \.title
),
PredicateExpressions.build_Arg("tom")
)
let expression2 = PredicateExpressions.build_contains(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(book),
keyPath: \.title
),
PredicateExpressions.build_Arg("the")
)
if useExpression1 {
conditions.append(expression1)
}
if useExpression2 {
conditions.append(expression2)
}
guard let first = conditions.first else {
return PredicateExpressions.Value(true)
}
let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = {
buildConjunction(lhs: $0, rhs: $1)
}
return conditions.dropFirst().reduce(first, closure)
})
}
}
#Preview {
ContentView()
}
My understanding of the intricacies of this code are limited, so please don't expect clever answers if you want to know more.
I look forward to some future iteration of SwiftData when this will become a lot easier.