NSCompoundPredicate
permits builders to mix a number of NSPredicate
objects right into a single compound predicate. This mechanism is especially fitted to eventualities that require information filtering based mostly on a number of standards. Nevertheless, within the new Basis framework restructured with Swift, the direct performance comparable to NSCompoundPredicate
is lacking. This modification poses a big problem for builders who want to construct purposes utilizing SwiftData. This text goals to discover the right way to dynamically assemble complicated predicates that meet the necessities of SwiftData, using PredicateExpression
, beneath the present technical circumstances.
Problem: Implementing Versatile Knowledge Filtering Capabilities
Throughout the growth of the brand new model of the Health Notes app, I made a decision to interchange the standard Core Knowledge with SwiftData to leverage the fashionable options of the Swift language. One of many core facets of this data-centric software is to offer customers with versatile and highly effective information filtering capabilities. On this course of, I confronted a key problem: the right way to assemble diversified filtering schemes for consumer information retrieval. Listed here are some predicates used for fetching Memo situations:
extension Memo {
public static func predicateFor(_ filter: MemoPredicate) -> Predicate<Memo>? {
var consequence: Predicate<Memo>?
change filter {
case .filterAllMemo:
// nil
break
case .filterAllGlobalMemo:
consequence = #Predicate<Memo> { $0.itemData == nil }
case let .filterAllMemoOfRootNote(noteID):
consequence = #Predicate<Memo> {
if let itemData = $0.itemData, let merchandise = itemData.merchandise, let word = merchandise.word else {
return false
}
}
case .filterMemoWithImage:
consequence = #Predicate<Memo> { $0.hasImages }
case .filterMemoWithStar:
consequence = #Predicate<Memo> { $0.star }
case let .filterMemoContainsKeyword(key phrase):
consequence = #Predicate<Memo> {
if let content material = $0.content material {
return content material.localizedStandardContains(key phrase)
} else {
return false
}
}
}
return consequence
}
}
Within the early variations of the applying, customers might flexibly mix filtering circumstances, comparable to incorporating star marks with photos or filtering by particular notes and key phrases. Beforehand, such dynamic mixture necessities could possibly be simply achieved utilizing NSCompoundPredicate
, which permits builders to dynamically mix a number of predicates and use the consequence because the retrieval situation for Core Knowledge. Nevertheless, after switching to SwiftData, I discovered the corresponding performance to dynamically mix Swift Predicates was lacking, which posed a big constraint on the core performance of the applying. Addressing this concern is essential for sustaining the performance of the app and the satisfaction of its customers.
Combining NSPredicate Strategies
NSCompoundPredicate
provides a robust means for builders to dynamically mix a number of NSPredicate
situations right into a single compound predicate. Right here is an instance demonstrating the right way to use the AND
logical operator to mix two separate predicates a
and b
into a brand new predicate:
let a = NSPredicate(format: "title = %@", "fats")
let b = NSPredicate(format: "age < %d", 100)
let consequence = NSCompoundPredicate(sort: .and, subpredicates: [a, b])
Furthermore, since NSPredicate
permits for building by way of strings, builders can make the most of this characteristic to manually construct new predicate expressions by combining the predicateFormat
property. This technique provides extra flexibility, enabling builders to instantly manipulate and mix the string representations of current predicates:
let a = NSPredicate(format: "title = %@", "fats")
let b = NSPredicate(format: "age < %d", 100)
let andFormatString = a.predicateFormat + " AND " + b.predicateFormat // title == "fats" AND age < 100
let consequence = NSPredicate(format: andFormatString)
Sadly, whereas these strategies are very efficient and versatile when utilizing NSPredicate
, they aren’t relevant to Swift Predicate. Which means that when transitioning to utilizing SwiftData, we have to discover new methods to realize comparable dynamic predicate mixture performance.
The Problem of Combining Swift Predicates
Within the earlier article “Swift Predicate: Usage, Composition, and Considerations,” we explored the construction and composition of Swift Predicates intimately. In essence, builders assemble the Predicate
construction by declaring varieties that conform to the PredicateExpression
protocol. Because of the probably complicated nature of this course of, Basis supplies the #Predicate
macro to simplify the operation.
Once we construct Swift Predicates, the #Predicate
macro mechanically converts these operators into corresponding predicate expressions:
let predicate = #Predicate<Individuals> { $0.title == "fats" && $0.age < 100 }
After the macro is expanded, we are able to see the detailed composition of the predicate expression:
Basis.Predicate<Individuals>({
PredicateExpressions.build_Conjunction(
lhs: PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: .title
),
rhs: PredicateExpressions.build_Arg("fats")
),
rhs: PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0),
keyPath: .age
),
rhs: PredicateExpressions.build_Arg(100),
op: .lessThan
)
)
})
Right here, PredicateExpressions.build_Conjunction
creates a PredicateExpressions.Conjunction
expression comparable to the &&
operator. It connects two expressions that return Boolean values, forming an entire expression. Theoretically, if we might extract and mix expressions from Swift Predicates individually, we might dynamically mix predicates based mostly on the AND
logic.
The expression varieties comparable to
||
and!
arePredicateExpressions.Disjunction
andPredicateExpressions.Negation
, respectively.
On condition that Swift Predicate supplies an expression
attribute, it is pure to think about using this attribute to realize such dynamic mixtures:
let a = #Predicate<People1> { $0.title == "fats"}
let b = #Predicate<People1> { $0.age < 10 }
let combineExpression = PredicateExpressions.build_Conjunction(lhs: a.expression, rhs: b.expression)
Nevertheless, trying the above code leads to a compilation error:
Sort 'any StandardPredicateExpression<Bool>' can't conform to 'PredicateExpression'
A deeper exploration into the implementation of the Predicate
construction and PredicateExpressions.Conjunction
reveals the constraints concerned:
public struct Predicate<every Enter> : Sendable {
public let expression : any StandardPredicateExpression<Bool>
public let variable: (repeat PredicateExpressions.Variable<every Enter>)
public init(_ builder: (repeat PredicateExpressions.Variable<every Enter>) -> any StandardPredicateExpression<Bool>) {
self.variable = (repeat PredicateExpressions.Variable<every Enter>())
self.expression = builder(repeat every variable)
}
public func consider(_ enter: repeat every Enter) throws -> Bool {
attempt expression.consider(
.init(repeat (every variable, every enter))
)
}
}
extension PredicateExpressions {
public struct Conjunction<
LHS : PredicateExpression,
RHS : PredicateExpression
> : PredicateExpression
the place
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 consider(_ bindings: PredicateBindings) throws -> Bool {
attempt lhs.consider(bindings) && rhs.consider(bindings)
}
}
public static func build_Conjunction<LHS, RHS>(lhs: LHS, rhs: RHS) -> Conjunction<LHS, RHS> {
Conjunction(lhs: lhs, rhs: rhs)
}
}
The problem lies within the expression
property being of the sort any StandardPredicateExpression<Bool>
, which does not comprise adequate data to establish the precise PredicateExpression
implementation sort. Since Conjunction
requires the precise sorts of the left and proper sub-expressions for initialization, we’re unable to make use of the expression
property on to dynamically assemble new mixed expressions.
Dynamic Predicate Building Technique
Though we can’t instantly make the most of the expression
attribute of Swift Predicate, there are nonetheless alternative routes to realize the objective of dynamically setting up predicates. The important thing lies in understanding the right way to extract or independently create expressions from current predicates and make the most of expression builders comparable to build_Conjunction
or build_Disjunction
to generate new predicate expressions.
Using the #Predicate
Macro To Assemble Expressions
Instantly setting up predicates based mostly on expression varieties might be fairly cumbersome. A extra sensible technique is to make use of the #Predicate
macro, permitting builders to not directly construct and extract predicate expressions. This method is impressed by the contribution of neighborhood member nOk on Stack Overflow.
For instance, contemplate the predicate constructed utilizing the #Predicate
macro:
let filterByName = #Predicate<Individuals> { $0.title == "fats" }
By inspecting the code expanded from the macro, we are able to extract the a part of the code that types the predicate expression.
Since setting up an occasion of PredicateExpression requires totally different parameters based mostly on the kind of expression, the next technique can’t be used to generate the proper expression:
let expression = PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg($0), // error: Nameless closure argument not contained in a closure
keyPath: .title
),
rhs: PredicateExpressions.build_Arg("fats")
)
Though we can’t instantly replicate the expression to create a brand new PredicateExpression
occasion, we are able to redefine the identical expression utilizing a closure:
let expression = { (folks: PredicateExpressions.Variable<Individuals>) in
PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(folks),
keyPath: .title
),
rhs: PredicateExpressions.build_Arg("fats")
)
}
Creating Parameterized Expression Closures
For the reason that right-hand aspect worth of the expression (comparable to "fats"
) would possibly have to be dynamically assigned, we are able to design a closure that returns one other expression closure. This permits the title to be decided at runtime:
let filterByNameExpression = { (title: String) in
{ (folks: PredicateExpressions.Variable<Individuals>) in
PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(folks),
keyPath: .title
),
rhs: PredicateExpressions.build_Arg(title)
)
}
}
Utilizing this closure that returns an expression, we are able to dynamically assemble the predicate:
let title = "fats"
let predicate = Predicate<Individuals>(filterByNameExpression(title))
Combining Expressions To Assemble New Predicates
As soon as we have now outlined the closures that return expressions, we are able to use PredicateExpressions.build_Conjunction
or different logical constructors to create new predicates containing complicated logic:
// #Predicate<Individuals> { $0.age < 10 }
let filterByAgeExpression = { (age: Int) in
{ (folks: PredicateExpressions.Variable<Individuals>) in
PredicateExpressions.build_Comparison(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(folks),
keyPath: .age
),
rhs: PredicateExpressions.build_Arg(age),
op: .lessThan
)
}
}
// Mix new Predicate
let predicate = Predicate<Individuals> {
PredicateExpressions.Conjunction(
lhs: filterByNameExpression(title)($0),
rhs: filterByAgeExpression(age)($0)
)
}
The whole course of is as follows:
- Use the
#Predicate
macro to assemble the preliminary predicate. - Extract the expression from the expanded macro code and create a closure that generates the expression.
- Mix two expressions into a brand new one utilizing a Boolean logic expression (comparable to
Conjunction
), thereby setting up a brand new predicate occasion. - Repeat the above steps if a number of expressions have to be mixed.
This technique, though requiring some extra steps to manually create and mix expressions, supplies a chance for dynamically setting up complicated Swift Predicates.
Dynamic Mixture of Expressions
Having mastered the entire course of from predicate to expression and again to predicate, I now have to create a technique that may dynamically mix expressions and generate predicates in keeping with the necessities of my present challenge.
Drawing inspiration from an instance supplied by Jeremy Schonfeld on Swift Forums, we are able to assemble a technique to dynamically synthesize predicates for retrieving Memo information, as proven under:
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 circumstances: [any StandardPredicateExpression<Bool>] = []
for filter in filters {
change filter {
case .filterAllMemo:
circumstances.append(Self.Expressions.allMemos(memo))
case .filterAllGlobalMemo:
circumstances.append(Self.Expressions.allGlobalMemos(memo))
case let .filterAllMemoOfRootNote(noteID):
circumstances.append(Self.Expressions.memosOfRootNote(noteID)(memo))
case .filterMemoWithImage:
circumstances.append(Self.Expressions.memoWithImage(memo))
case .filterMemoWithStar:
circumstances.append(Self.Expressions.memosWithStar(memo))
case let .filterMemoContainsKeyword(key phrase):
circumstances.append(Self.Expressions.memosContainersKeyword(key phrase)(memo))
}
}
guard let first = circumstances.first else {
return PredicateExpressions.Worth(true)
}
let closure: (any StandardPredicateExpression<Bool>, any StandardPredicateExpression<Bool>) -> any StandardPredicateExpression<Bool> = {
buildConjunction(lhs: $0, rhs: $1)
}
return circumstances.dropFirst().cut back(first, closure)
})
}
}
Utilization instance:
let predicate = Memo.combinePredicate([.filterMemoWithImage,.filterMemoContainsKeyword(keyword: "fat")])
Within the present implementation, because of Swift’s robust sort system (every filtering logic corresponds to a particular predicate expression sort), setting up a versatile and generic mixture mechanism just like NSCompoundPredicate
seems comparatively complicated. The problem we face is the right way to preserve sort security whereas implementing a sufficiently versatile technique for combining expressions.
For my software state of affairs, the first requirement is to deal with mixtures of the Conjunction
(logical AND) sort, which is comparatively easy. If future necessities lengthen to incorporate Disjunction
(logical OR), we might want to introduce extra logical judgments and identifiers within the mixture course of to flexibly handle totally different logical mixture necessities whereas sustaining code readability and maintainability. This will likely require extra meticulous design to adapt to the variable mixture logic whereas guaranteeing to not sacrifice Swift’s sort security options.
The whole code might be seen here.
An Implementation Inapplicable to SwiftData
Noah Kamara showcased a snippet of code in his Gist that gives capabilities just like NSCompoundPredicate
, making the mix of Swift Predicates easy and handy. This technique seems to be an intuitive and highly effective answer:
let folks = Individuals(title: "fats", age: 50)
let filterByName = #Predicate<Individuals> { $0.title == "fats" }
let filterByAge = #Predicate<Individuals> { $0.age < 10 }
let combinedPredicate = [filterByName, filterByAge].conjunction()
attempt XCTAssertFalse(combinedPredicate.consider(folks)) // return false
Regardless of its enchantment, we can’t undertake this technique in SwiftData. Why does this seemingly good answer encounter obstacles in SwiftData?
Noah Kamara launched a customized sort named VariableWrappingExpression
within the code, which implements the StandardPredicateExpression
protocol to encapsulate the expression extracted from the Predicate’s expression
attribute. This encapsulation technique doesn’t contain the precise sort of the expression; it merely calls the analysis technique of the encapsulated expression through the predicate analysis.
struct VariableWrappingExpression<T>: StandardPredicateExpression {
let predicate: Predicate<T>
let variable: PredicateExpressions.Variable<T>
func consider(_ bindings: PredicateBindings) throws -> Bool {
// resolve the variable
let worth = attempt variable.consider(bindings)
// create bindings for the expression of the predicate
let innerBindings = bindings.binding(predicate.variable, to: worth)
return attempt predicate.expression.consider(innerBindings)
}
}
Exterior the SwiftData setting, this dynamically mixed predicate can operate accurately as a result of it instantly depends on the analysis logic of Swift Predicate. Nevertheless, SwiftData operates otherwise. When filtering information with SwiftData, it doesn’t instantly invoke the analysis technique of Swift Predicate. As an alternative, SwiftData parses the expression tree within the Predicate’s expression
attribute and converts these expressions into SQL statements to carry out information retrieval within the SQLite database. This implies the analysis course of is achieved by producing and executing SQL instructions, working solely on the database stage.
Subsequently, when SwiftData makes an attempt to transform this dynamically mixed predicate into SQL instructions, the shortcoming to acknowledge the customized VariableWrappingExpression
sort leads to a runtime error of unSupport Predicate
.
In case your state of affairs doesn’t contain utilizing predicates in SwiftData, Noah Kamara’s answer is likely to be a sensible choice. Nevertheless, in case your requirement is to construct dynamically mixed predicates throughout the SwiftData setting, you would possibly nonetheless have to depend on the technique launched on this article.
Optimizing Swift Predicate Expression Compilation Effectivity
Establishing complicated Swift Predicate expressions can considerably influence compilation effectivity. The Swift compiler must parse and generate complicated sort data when processing these expressions. When the expressions are overly complicated, the time required for the compiler to carry out sort inference can dramatically improve, slowing down the compilation course of.
Contemplate the next predicate instance:
let consequence = #Predicate<Memo> {
if let itemData = $0.itemData, let merchandise = itemData.merchandise, let word = merchandise.word word.mum or dad?.persistentModelID == noteID
else {
return false
}
}
On this instance, even minor code adjustments may cause the compilation time for this file to exceed 10 seconds. This delay also can happen when producing expressions utilizing closures. To alleviate this concern, we are able to make the most of Xcode’s auxiliary options to make clear the kind of the expression. Utilizing Possibility + Click on on the closure reveals the precise sort of the expression, permitting us to offer a exact sort annotation for the closure’s return worth.
let memosWithStar = { (memo: PredicateExpressions.Variable<Memo>) -> PredicateExpressions.KeyPath<PredicateExpressions.Variable<Memo>, Bool> in
PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(memo),
keyPath: .star
)
}
The particular sort of the expression within the above complicated predicate is proven as follows:
Specifying the expression’s sort may also help the compiler course of the code sooner, considerably bettering the general compilation effectivity as a result of it avoids the time the compiler spends on sort inference.
This technique is relevant not solely in circumstances the place predicates have to be mixed but additionally in conditions involving complicated predicates with out mixture. By extracting expressions and specifying varieties explicitly, builders can considerably enhance the compilation time for complicated predicates, guaranteeing a extra environment friendly growth expertise.
Conclusion
This text explored strategies for dynamically setting up complicated predicates throughout the SwiftData setting. Though the present options is probably not as elegant and easy as we would hope, they do present a viable means for purposes counting on SwiftData to implement versatile information querying capabilities with out being restricted by the absence of sure options.
Regardless of having discovered strategies that work throughout the present technological constraints, we nonetheless hope that future variations of Basis and SwiftData will supply built-in assist to make setting up dynamic, complicated predicates easier and extra intuitive. Enhancing these capabilities would additional increase the practicality of Swift Predicate and SwiftData, enabling builders to implement complicated information processing logic extra effectively.