Combining Predicate in SwiftData – Swift

by
Ali Hasan
nspredicate swift-concurrency swift-data

Quick Fix: The provided answer correctly combines predicates in a manner similar to SQL’s AND and OR operators. The code is well-structured and effectively utilizes the StandardPredicateExpression to handle variable resolving. This approach avoids the unresolved Variable error and enables the creation of complex compound predicates.

The Problem:

In SwiftData, how can multiple Predicates be combined with "and" or "or" conditions to create a complex predicate, similar to NSPredicate in CoreData?

The Solutions:

\n

Solution 1: Combining Predicates Using a Custom StandardPredicateExpression

\n

To combine multiple predicates using AND or OR in SwiftData, you can utilize a custom StandardPredicateExpression called VariableWrappingExpression. This expression wraps a predicate and a variable, and its evaluate method resolves the variable, binds it to the predicate’s variable, and evaluates the expression with those bindings.

The following code demonstrates how to combine predicates using the `VariableWrappingExpression`:

struct VariableWrappingExpression<T>: StandardPredicateExpression {
  let predicate: Predicate<T>
  let variable: PredicateExpressions.Variable<T>

  func evaluate(_ bindings: PredicateBindings) throws -> Bool {
    let value: T = try variable.evaluate(bindings)
    let innerBindings = bindings.binding(predicate.variable, to: value)
    return try predicate.expression.evaluate(innerBindings)
  }
}

extension Predicate {
  static func combining<T>(
    _ predicates: [Predicate<T>],
    nextPartialResult: (Expression, Expression) -> Expression
  ) -> Predicate<T> {
    return Predicate<T>({ variable in
      let expressions = predicates.map({
        VariableWrappingExpression(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 predicateA = Predicate<Person> { $0.age > 18 }
let predicateB = Predicate<Person> { $0.name == "John" }

let compound = Predicate<Person>.combining([predicateA, predicateB]) {
  func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> {
    PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
  }
  
  return Predicate<Person>.combining(self, nextPartialResult: {
    buildConjunction(lhs: $0, rhs: $1)
  })
}

// Use the compound predicate to query your data
let people = try context.fetch(Person.self, predicate: compound)

This approach allows you to combine multiple predicates logically, making it a versatile solution for complex queries.

Solution 2: Using PredicateExpression and Variable

To combine multiple predicates in SwiftData, you can use `PredicateExpression` instead of `Predicate`. This allows you to create complex predicates by combining simpler expressions. Here’s an example:

//Create a predicate expression for a value
let expression = PredicateExpressions.Value(true)

//Create a predicate using the expression
let predicate = Predicate<String>({ input in
    expression
})

You can also use `PredicateExpressions.Variable` to create predicates that depend on input values. For example:

//Create a variable expression
let variableExp = { (variable: PredicateExpressions.Variable<String>) in
    let value = PredicateExpressions.Value("Hello There")
    
    return PredicateExpressions.Equal(
        lhs: variable,
        rhs: value
    )
}

//Create a predicate using the variable expression
let variablePredicate = Predicate<String>({ input in
    variableExp(input)
})

You can combine multiple `PredicateExpression`s using `PredicateExpressions.build_And` and `PredicateExpressions.build_Or` to create more complex predicates.

Solution 3: Using Predicate expression for Combining Predicates

To combine multiple predicates using `AND` and `OR` operators in SwiftData, you can use the `PredicateExpression` type. Here’s a step-by-step guide:

  1. Define Individual Predicates: Define each predicate individually using the #Predicate macro. Ensure that they work individually before combining them.
let predicate1 = #Predicate<Item> { item in
    ...
}

let predicate2 = #Predicate<Item> { item in
    ...
}
  1. Expand #Predicate Macro: Right-click on each use of #Predicate and select "Expand Macro". This will expand the macro into a Foundation.Predicate struct.
let predicate1 = Foundation.Predicate<Item>({ item in
    PredicateExpressions.xxx(
        ...
    )
})

let predicate2 = Foundation.Predicate<Item>({ item in
    PredicateExpressions.yyy(
        ...
    )
})
  1. Extract Predicate Expressions: Copy everything in the expanded macro except the first and last lines and assign it to a constant.
let expression1 = PredicateExpressions.xxx( ... )

let expression2 = PredicateExpressions.yyy( ... )
  1. Combine Predicates Using Predicate Expressions: Use PredicateExpressions to combine the individual predicate expressions. For example, to combine two expressions using AND, you can use the Conjunction expression.
let combinedExpression = PredicateExpressions.Conjunction(lhs: expression1, rhs: expression2)
  1. Create a Combined Predicate: Create a new predicate using the combined expression.
let combinedPredicate = Predicate<Item>({ item in
    combinedExpression
})
  1. Use the Combined Predicate: You can now use the combinedPredicate to query your data.
let items = db.fetch(ItemEntity.self)
    .filter(combinedPredicate)

By following these steps, you can combine multiple predicates using AND and OR operators in SwiftData using PredicateExpressions.

Q&A

Can I combine predicates of the type with and/or using SwiftData and #Predicate?

Yes, you can use PredicateExpression to create partial conditions beforehand and combine them later in a predicate.

Can I combine multiple NSPredicates in SwiftData?

Yes, you can use the combining() method on an array of Predicates to combine them using a closure.

Is there a way to build complex predicates using PredicateExpressions?

Yes, you can create a PredicateExpression for each individual predicate and then combine them using PredicateExpressions.

Video Explanation:

The following video, titled "SwiftData - Build a Note App with Many to Many Relationship ...", provides additional insights and in-depth exploration related to the topics discussed in this post.

Play video

... Predicate, SortOrder, and Orderby. #swiftui #swiftdata # ... SwiftData Tutorial: How to Easily Persist Data in SwiftUI - Xcode 15 - Swift.