NSCompoundPredicate
allows developers to combine more NSPredicate
objects into one complex predicate. This mechanism is particularly suitable for scenarios that require data filtering based on multiple criteria. However, in the new Foundation framework restructured with Swift, the direct functionality fits NSCompoundPredicate
is missing. This change presents a significant challenge for developers who want to build applications using SwiftData. This article aims to explore how to dynamically construct complex predicates that meet the requirements of SwiftData, using PredicateExpression
according to the valid technical conditions.
Challenge: Implementing flexible data filtering capabilities
During the development of the new version of the Health Notes application, I decided to replace the traditional Core Data with SwiftData in order to take advantage of the modern features of the Swift language. One of the key aspects of this data-centric application is to provide users with flexible and powerful data filtering capabilities. In this process, I faced a key challenge: how to construct diverse filtering schemes to retrieve user data. Here are some predicates used to retrieve Memo instances:
extension Memo {
public static func predicateFor(_ filter: MemoPredicate) -> Predicate<Memo>?
var result: Predicate<Memo>?
switch filter
case .filterAllMemo:
// nil
break
case .filterAllGlobalMemo:
result = #Predicate<Memo> $0.itemData == nil
case let .filterAllMemoOfRootNote(noteID):
result = #Predicate<Memo>
if let itemData = $0.itemData, let item = itemData.item, let note = item.note note.parent?.persistentModelID == noteID
else
return false
case .filterMemoWithImage:
result = #Predicate<Memo> $0.hasImages
case .filterMemoWithStar:
result = #Predicate<Memo> $0.star
case let .filterMemoContainsKeyword(keyword):
result = #Predicate<Memo>
if let content = $0.content
return content.localizedStandardContains(keyword)
else
return false
return result
}
In the first versions of the application, users could flexibly combine filtering conditions, such as including stars with images or filtering by specific notes and keywords. Previously, such dynamic combination requirements could easily be achieved using NSCompoundPredicate
, which allows developers to dynamically combine multiple predicates and use the results as conditions to retrieve underlying data. However, after switching to SwiftData, I found that the proper functionality for dynamically combining Swift predicates was missing, which was a significant limitation to the core functionality of the application. Solving this problem is essential to maintain the functionality of the application and the satisfaction of its users.
Combining NSPredicate methods
NSCompoundPredicate
offers a powerful way for developers to dynamically combine multiple NSPredicate
instances into one complex predicate. Here is an example that shows how to use AND
logical operator for combining two separate predicates a
and b
into a new predicate:
let a = NSPredicate(format: "name = %@", "fat")
let b = NSPredicate(format: "age < %d", 100)
let result = NSCompoundPredicate(type: .and, subpredicates: [a, b])
Moreover, since NSPredicate
allows construction via arrays, developers can use this feature to manually construct new predicate expressions by combining them predicateFormat
property. This method offers additional flexibility, allowing developers to directly manipulate and combine string representations of existing predicates:
let a = NSPredicate(format: "name = %@", "fat")
let b = NSPredicate(format: "age < %d", 100)
let andFormatString = a.predicateFormat + " AND " + b.predicateFormat // name == "fat" AND age < 100
let result = NSPredicate(format: andFormatString)
Unfortunately, while these methods are very effective and flexible in use NSPredicate
, are not applicable to the Swift predicate. This means that when transitioning to using SwiftData, we need to explore new ways to achieve similar dynamic predicate combination functionality.
The challenge of combining Swift predicates
In the previous article, “Swift Predicate: Usage, Composition, and Considerations,” we explored the structure and composition of Swift predicates in detail. In essence, developers construct Predicate
structure by declaring the corresponding types PredicateExpression
protocol. Due to the potentially complex nature of this process, the Foundation provides #Predicate
macro to simplify work.
When we build Swift predicates, #Predicate
the macro automatically converts these operators into corresponding predicate expressions:
let predicate = #Predicate<People> $0.name == "fat" && $0.age < 100
After the macro is expanded, we can see the detailed composition of the predicate expression:
Foundation.Predicate<People>(
PredicateExpressions.build_Conjunction(
lhs: PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.name
),
rhs: PredicateExpressions.build_Arg("fat")
),
rhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: \.age
),
rhs: PredicateExpressions.build_Arg(100),
op: .lessThan
)
)
)
Here, PredicateExpressions.build_Conjunction
creates a PredicateExpressions.Conjunction
matching expression &&
operator. It concatenates two expressions that return Boolean values, forming a complete expression. In theory, if we could extract and combine expressions from Swift predicates individually, we could dynamically combine predicates based on AND
logic.
Matching expression types
||
and!
arePredicateExpressions.Disjunction
andPredicateExpressions.Negation
respectively.
Given that the Swift predicate provides expression
attribute, it is natural to consider using this attribute to achieve such dynamic combinations:
let a = #Predicate<People1> $0.name == "fat"
let b = #Predicate<People1> $0.age < 10
let combineExpression = PredicateExpressions.build_Conjunction(lhs: a.expression, rhs: b.expression)
However, attempting the code above results in a compilation error:
Type 'any StandardPredicateExpression<Bool>' cannot conform to 'PredicateExpression'
Deeper implementation research Predicate
structure and PredicateExpressions.Conjunction
reveals the limitations involved:
public struct Predicate<each Input> : Sendable
public let expression : any StandardPredicateExpression<Bool>
public let variable: (repeat PredicateExpressions.Variable<each Input>)
public init(_ builder: (repeat PredicateExpressions.Variable<each Input>) -> any StandardPredicateExpression<Bool>)
self.variable = (repeat PredicateExpressions.Variable<each Input>())
self.expression = builder(repeat each variable)
public func evaluate(_ input: repeat each Input) throws -> Bool
try expression.evaluate(
.init(repeat (each variable, each input))
)
extension PredicateExpressions
public struct Conjunction<
LHS : PredicateExpression,
RHS : PredicateExpression
> : PredicateExpression
where
LHS.Output == Bool,
RHS.Output == Bool
public typealias Output = Bool
public let lhs: LHS
public let rhs: RHS
public init(lhs: LHS, rhs: RHS)
self.lhs = lhs
self.rhs = rhs
public func evaluate(_ bindings: PredicateBindings) throws -> Bool
try lhs.evaluate(bindings) && rhs.evaluate(bindings)
public static func build_Conjunction<LHS, RHS>(lhs: LHS, rhs: RHS) -> Conjunction<LHS, RHS>
Conjunction(lhs: lhs, rhs: rhs)
The problem lies in expression
a property that is of type any StandardPredicateExpression<Bool>
which does not contain enough information to identify a particular one PredicateExpression
type of implementation. From Conjunction
requires exact types of left and right subexpressions to initialize, we cannot use expression
property directly for the dynamic construction of new combined expressions.
Dynamic predicate construction strategy
Although we cannot use directly expression
attributes of Swift predicates, there are still alternative ways to achieve the goal of dynamic predicate construction. The key lies in understanding how to extract or independently create expressions from existing predicates and use expression building tools such as build_Conjunction
or build_Disjunction
for generating new predicate expressions.
Being used #Predicate
A macro for constructing expressions
Directly constructing predicates based on expression types can be quite cumbersome. A more practical method is to use it #Predicate
macros, allowing programmers to indirectly build and extract predicate expressions. This approach was inspired by a contribution from community member nOk on Stack Overflow.
For example, consider the predicate constructed using #Predicate
macro:
let filterByName = #Predicate<People> $0.name == "fat"
By examining the code expanded from the macro, we can extract the part of the code that forms the predicate expression.
Because constructing a PredicateExpression instance requires different parameters based on the type of expression, the following method cannot be used to generate a valid expression:
let expression = PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0), // error: Anonymous closure argument not contained in a closure
keyPath: \.name
),
rhs: PredicateExpressions.build_Arg("fat")
)
Although we cannot directly replicate an expression to create a new one PredicateExpression
for example, we can redefine the same expression using a closure:
let expression = (people: PredicateExpressions.Variable<People>) in
PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(people),
keyPath: \.name
),
rhs: PredicateExpressions.build_Arg("fat")
)
Creating parameterized expression closures
Since the value of the right-hand side of an expression (such as "fat"
) may need to be dynamically assigned, we can design a closure that returns another expression closure. This allows the name to be specified at runtime:
let filterByNameExpression = (name: String) in
(people: PredicateExpressions.Variable<People>) in
PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(people),
keyPath: \.name
),
rhs: PredicateExpressions.build_Arg(name)
)
Using this closure that returns an expression, we can dynamically construct a predicate:
let name = "fat"
let predicate = Predicate<People>(filterByNameExpression(name))
Combining expressions to construct new predicates
Once we have defined closures that return expressions, we can use PredicateExpressions.build_Conjunction
or other logical constructors to create new predicates containing complex logic:
// #Predicate<People> $0.age < 10
let filterByAgeExpression = (age: Int) in
(people: PredicateExpressions.Variable<People>) in
PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(people),
keyPath: \.age
),
rhs: PredicateExpressions.build_Arg(age),
op: .lessThan
)
// Combine new Predicate
let predicate = Predicate<People>
PredicateExpressions.Conjunction(
lhs: filterByNameExpression(name)($0),
rhs: filterByAgeExpression(age)($0)
)
The complete process is as follows:
- Use
#Predicate
macro to construct the initial predicate. - Extract the expression from the extended macro code and create a closure that generates the expression.
- Combine two expressions into a new one using a Boolean expression (such as
Conjunction
), which constructs a new instance of the predicate. - Repeat the above steps if more expressions need to be combined.
This method, while requiring some additional steps to manually create and combine expressions, provides the ability to dynamically construct complex Swift predicates.
Dynamic combination of expressions
Having mastered the entire process from predicate to expression and back to predicate, I now need to create a method that can dynamically combine expressions and generate predicates according to the requirements of my current project.
Taking inspiration from an example provided by Jeremy Schonfeld on the Swift forums, we can construct a method to dynamically synthesize predicates to retrieve Memo data, as shown below:
extension Memo {
static func combinePredicate(_ filters: [MemoPredicate]) -> Predicate<Memo>
func buildConjunction(lhs: some StandardPredicateExpression<Bool>, rhs: some StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool>
PredicateExpressions.Conjunction(lhs: lhs, rhs: rhs)
return Predicate<Memo>( memo in
var conditions: [any StandardPredicateExpression<Bool>] = []
for filter in filters
switch filter
case .filterAllMemo:
conditions.append(Self.Expressions.allMemos(memo))
case .filterAllGlobalMemo:
conditions.append(Self.Expressions.allGlobalMemos(memo))
case let .filterAllMemoOfRootNote(noteID):
conditions.append(Self.Expressions.memosOfRootNote(noteID)(memo))
case .filterMemoWithImage:
conditions.append(Self.Expressions.memoWithImage(memo))
case .filterMemoWithStar:
conditions.append(Self.Expressions.memosWithStar(memo))
case let .filterMemoContainsKeyword(keyword):
conditions.append(Self.Expressions.memosContainersKeyword(keyword)(memo))
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)
)
}
Example of use:
let predicate = Memo.combinePredicate([.filterMemoWithImage,.filterMemoContainsKeyword(keyword: "fat")])
In the current implementation, due to Swift’s strong type system (each filtering logic corresponds to a specific type of predicate expression), constructing a flexible and generic combination mechanism similar to NSCompoundPredicate
looks relatively complex. The challenge we face is how to maintain type safety while implementing a sufficiently flexible strategy for combining expressions.
For my application scenario, the primary requirement is to handle combinations Conjunction
(logical I) type, which is relatively simple. If future requirements expand to include Disjunction
(logical OR), we will need to introduce additional logical judgments and identifiers in the combining process to flexibly respond to different logical combination requirements while maintaining code readability and maintainability. This may require a more meticulous design to accommodate variable combination logic while ensuring that Swift’s type safety features are not sacrificed.
You can see the complete code here.
Implementation not applicable to SwiftData
Noah Kamara presented a code snippet in his Gist that provides similar capabilities NSCompoundPredicate
, making the combination of Swift predicates simple and convenient. This method seems to be an intuitive and powerful solution:
let people = People(name: "fat", age: 50)
let filterByName = #Predicate<People> $0.name == "fat"
let filterByAge = #Predicate<People> $0.age < 10
let combinedPredicate = [filterByName, filterByAge].conjunction()
try XCTAssertFalse(combinedPredicate.evaluate(people)) // return false
Despite its appeal, we cannot adopt this method in SwiftData. Why does this seemingly perfect solution run into obstacles in SwiftData?
Noah Kamara introduced a custom type called VariableWrappingExpression
in the code it implements StandardPredicateExpression
a protocol for encapsulating an expression extracted from a predicate expression
attribute. This encapsulation method does not involve a specific type of expression; it just calls the evaluation method of the encapsulated expression during predicate evaluation.
struct VariableWrappingExpression<T>: StandardPredicateExpression
let predicate: Predicate<T>
let variable: PredicateExpressions.Variable<T>
func evaluate(_ bindings: PredicateBindings) throws -> Bool
// resolve the variable
let value = try variable.evaluate(bindings)
// create bindings for the expression of the predicate
let innerBindings = bindings.binding(predicate.variable, to: value)
return try predicate.expression.evaluate(innerBindings)
Outside of a SwiftData environment, this dynamic combined predicate can work correctly because it directly relies on Swift predicate evaluation logic. However, SwiftData works differently. When filtering data using SwiftData, the evaluate method of the Swift predicate is not called directly. Instead, SwiftData parses the expression tree in the predicate expression
attribute and converts those expressions into SQL statements to perform data retrieval in the SQLite database. This means that the evaluation process is achieved by generating and executing SQL statements, operating entirely at the database level.
Therefore, when SwiftData tries to convert this dynamic combined predicate into SQL statements, the inability to recognize the custom VariableWrappingExpression
type results in a runtime error of unSupport Predicate
.
If your scenario does not involve using predicates in SwiftData, Noah Kamara’s solution might be a good choice. However, if your requirement is to build dynamically combined predicates within the SwiftData environment, you may still need to rely on the strategy presented in this article.
Optimizing Swift predicate expression compilation efficiency
Constructing complex Swift predicate expressions can significantly affect compilation performance. The Swift compiler needs to parse and generate complex type information when processing these expressions. When expressions are too complex, the time it takes the compiler to perform type inference can increase dramatically, slowing down the compilation process.
Consider the following predicate example:
let result = #Predicate<Memo>
if let itemData = $0.itemData, let item = itemData.item, let note = item.note else
return false
In this example, even minor code changes can cause the compile time for this file to exceed 10 seconds. This delay can also occur when generating expressions using closures. To alleviate this problem, we can use Xcode’s helper features to disambiguate the expression type. Using the +click option on the closure reveals the exact type of the expression, allowing us to provide a precise type label for the closure’s return value.
let memosWithStar = (memo: PredicateExpressions.Variable<Memo>) -> PredicateExpressions.KeyPath<PredicateExpressions.Variable<Memo>, Bool> in
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(memo),
keyPath: \.star
)
The specific type of expression in the complex predicate above is shown as follows:
Determining the type of an expression can help the compiler process code faster, significantly improving overall compilation efficiency because it avoids the time the compiler spends on type inference.
This strategy is applicable not only in cases where it is necessary to combine predicates, but also in situations involving complex predicates without combination. By extracting expressions and specifying types explicitly, developers can significantly improve compile time for complex predicates, providing a more efficient development experience.
Conclusion
This article explored methods for dynamically constructing complex predicates within the SwiftData environment. While the current solutions may not be as elegant and simple as hoped, they provide a viable way for applications that rely on SwiftData to implement flexible querying capabilities without being limited by the lack of certain features.
Although we have found methods that work within current technological limitations, we still hope that future versions of Foundation and SwiftData will offer built-in support to make the construction of dynamic, complex predicates simpler and more intuitive. Improving these capabilities would further increase the practicality of Swift Predicate and SwiftData, allowing developers to more efficiently implement complex data processing logic.