I'm trying to combine multiple Predicates of the Type with and / or. Previously with CoreData and NSPredicate I'd just do this:
let predicate = NSPredicate(value: true)
let predicate2 = NSPredicate(value: false)
let combinedPred = NSCompoundPredicate(type: .or, subpredicates: [predicate, predicate2])
Is there a comparable way to do this using SwiftData and #Predicate? And if not, how could I implement a way to create partial conditions beforehand and combine them in a predicate later?
The only way I've found of doing this as an expression is like this, but this would make my predicate hundredths of lines long
let includeOnlyFavorites = true
#Predicate { includeOnlyFavorites ? $0.isFavorite : true }
I'm developing an App that allows users to save and query items using shortcut actions. The items are stored using SwiftData and queried using EntityPropertyQuery
Apple implements the Query properties like this:
static var properties = QueryProperties {
Property(\BookEntity.$title) {
EqualToComparator { NSPredicate(format: "title = %@", $0) }
ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
}
}
and later combines the predicates with NSCompoundPredicate
.
Closure with Bool return:
let isFavorite = { (item: Item) in item.isFavorite }
let predicate = #Predicate<Item> { isFavorite($0) }
evaluate(Item) -> Bool
method but I also can't use thatI also thought i might be able to use StandardPredicateExpression in another predicate because in the documentation it reads:
"A component expression that makes up part of a predicate, and that's supported by the standard predicate type." but there are no further explanations on this type
I've build a library that implements this for predicate expression supported by SwiftData. (If you're targeting iOS 17.4 (or equivalent) there are also instructions on how to do it the new builtin way)
https://github.com/NoahKamara/CompoundPredicate/
Building on the answer by @orgtre
TLDR: This Gist implements two methods conjunction()
and disjunction()
on Array<Predicate<T>>
the reason for the error and subsequent crash is that the PredicateExpressions.Variable
is used to resolve the Predicate input.
This is how Predicate Variable reolving works internally:
The Predicate you create looks something like this (when expanded):
let predicate = Foundation.Predicate<Person>({
PredicateExpressions.build_contains(
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.name
),
PredicateExpressions.build_Arg("Luke")
)
})
The closure takes parameters of PredicateExpressions.Variable<Input>
which you need to pass as an argument to your expression $0
This variable will be unique for every predicate you created, meaning when you combine them using just the predicate.expression
property, each expression has a distinct Variable leading to a unresolved Variable
error.
I created a custom StandardPredicateExpression
that takes a predicate and a variable and will do the following in it's evaluate method:
struct VariableWrappingExpression<T>: StandardPredicateExpression {
let predicate: Predicate<T>
let variable: PredicateExpressions.Variable<T>
func evaluate(_ bindings: PredicateBindings) throws -> Bool {
// resolve the variable
let value: T = try variable.evaluate(bindings)
// bind the variable of the predicate to this value
let innerBindings = bindings.binding(predicate.variable, to: value)
// evaluate the expression with those bindings
return try predicate.expression.evaluate(innerBindings)
}
}
Extending the excellent work by @orgtre to create a solution that takes an array of predicates and a closure for combining them
extension Predicate {
typealias Expression = any StandardPredicateExpression<Bool>
static func combining<T>(
_ predicates: [Predicate<T>],
nextPartialResult: (Expression, Expression) -> Expression
) -> Predicate<T> {
return Predicate<T>({ variable in
let expressions = predicates.map({
VariableWrappingExpression<T>(predicate: $0, variable: variable)
})
guard let first = expressions.first else {
return PredicateExpressions.Value(true)
}
let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = {
nextPartialResult($0,$1)
}
return expressions.dropFirst().reduce(first, closure)
})
}
}
let compound = Predicate<Person>.combine([predicateA, predicateB]) {
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
}
return Predicate<T>.combining(self, nextPartialResult: {
buildConjunction(lhs: $0, rhs: $1)
})
}
Check this Gist for an implementation