Dynamic Swift Predicates in macOS 14 and iOS 17
The new Foundation/#Swiftlang
Predicate
s (and its expressions) seem a little weird because they can’t be constructed dynamically?
However, when I try to use that method inside of a
#Predicate
closure, it gives an error saying that the method is not supported by this predicate.
If you want to construct a
Predicate
dynamically, you need to build it up fromPredicateExpression
pieces rather than use the#Predicate
macro (similar to building anNSPredicate
fromNSExpression
). Expand the macro in Xcode and you can see how the pieces are put together.
It’s a lot more complicated than NSPredicate
due to the static typing, and there’s no way to convert between the two types if you’re using both Core Data and SwiftData.
NSCompoundPredicate
allows developers to combine multipleNSPredicate
objects into a single compound predicate. This mechanism is particularly suited for scenarios that require data filtering based on multiple criteria. However, in the new Foundation framework restructured with Swift, the direct functionality corresponding toNSCompoundPredicate
is missing. This change poses a significant challenge for developers who wish to build applications using SwiftData. This article aims to explore how to dynamically construct complex predicates that meet the requirements of SwiftData, utilizingPredicateExpression
, under the current technical conditions.[…]
The issue lies in the
expression
property being of the typeany StandardPredicateExpression<Bool>
, which doesn’t contain sufficient information to identify the specificPredicateExpression
implementation type. SinceConjunction
requires the exact types of the left and right sub-expressions for initialization, we are unable to use theexpression
property directly to dynamically construct new combined expressions.[…]
Although we cannot directly utilize the
expression
attribute of Swift Predicate, there are still alternative ways to achieve the goal of dynamically constructing predicates. The key lies in understanding how to extract or independently create expressions from existing predicates and utilize expression builders such asbuild_Conjunction
orbuild_Disjunction
to generate new predicate expressions.
If you need to dynamically create a predicate while analyzing what should go into the predicate, you can do so by manually constructing the expression tree. Unfortunately, since this is a more advanced use case you wouldn't be able to use the macro to help here, but you could write something along the lines of the following[…]
[…]
In short, we create a list of the conditions that need to be met and build up the list based on which parameters are specified to the
makePredicate
function. We can thenreduce
this array into a single tree of conjunctions to ensure that all of the conditions are met. There are a few small hoops to jump through here in order to satisfy the type-checker with the use of generics such as theclosure
and separatebuildConjunction
function, but this allows you to just append toconditions
for each new property rather than needing to work with a combinatorial explosion of conditions using the macro.
CompoundPredicate aims to improve the
Predicate
system to enable combining multiple predicates after constructing them[…]
This looks like a huge improvement. It’s not clear to me whether there are still limitations compared with NSPredicate
.
This new strategy abandons the previous reliance on a custom
StandardPredicateExpression
implementation, opting instead for a type-casting strategy that effectively concretizes the information ofexpression
. This improvement means developers can avoid the cumbersome process of manually extracting and combining expressions.[…]
This method enables the automatic acquisition of the exact type of expressions inside the
Predicate
during the predicate combination process, facilitating an automated and efficient combination of predicates.
You know how Foundation (in part to support SwiftData) now has the
Predicate
macro? Well, RealityKit has its own genericQueryPredicate
And guess what, they don’t need a macro to build them, looks like overloading the operators||
and&&
is fine there.
Operator overloads not only cause longer build times for
Predicate
, but for other uses of that operator.
But even with macros:
In this example, even minor code changes can cause the compilation time for this file to exceed 10 seconds. This delay can also occur when generating expressions using closures.
Using generics to create SwiftData
Predicate
s leads to crashy times.[…]
FYI, I solved this by writing another macro.
This is my new mantra: code not working? You don't have enough macros.
Since
Predicate
is bothCodable
andSendable
, it requires that everything the predicate captures (i.e. all instances captured by the closure) are alsoCodable
andSendable
.
Previously:
- Building, Testing, and Scaling With SwiftUI
- SwiftData
- NSPredicate, Core Data, and NULL
- Swift Pitch: Predicates
3 Comments RSS · Twitter · Mastodon
Related to the compilation weirdness, I found them frustrating to use because debugging the ‘builder’ thing is so painful, its a similar experience to writing SwiftUI.
Reading this kinda seems to suggest that it is possible to build Predicate's dynamically. While I found a wild way doing this using Codable (still not released), I don't think this is actually possible using regular API, and somewhat confirmed by this: https://mastodon.social/@dgoldsmith/110733729085796245
@Helge I’m still confused about this, perhaps because I haven’t tried doing it myself yet. I think the summary is that you can combine pieces that are defined statically but that you can’t construct them entirely dynamically. But for any fixed problem, e.g. a smart folder where know all the possibilities ahead of time, maybe that’s enough (though awkward)?
I’m sorry to hear that your solution uses Codable
because that was the only escape hatch out of the static typing that I found—was hoping you had something better.