swiftnspredicatepredicateswiftdata

Combining Predicate in SwiftData


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 }

Context:

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.


Tried and failed:

Closure with Bool return:

let isFavorite = { (item: Item) in item.isFavorite }
let predicate = #Predicate<Item> { isFavorite($0) }

I 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


Solution

  • 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/

    Old Answer / Implementation details of 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