Tuesday, May 28, 2024

Dynamic Swift Predicates in macOS 14 and iOS 17

Helge Heß:

The new Foundation/#Swiftlang Predicates (and its expressions) seem a little weird because they can’t be constructed dynamically?

Fly0strich:

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.

Debbie Goldsmith:

If you want to construct a Predicate dynamically, you need to build it up from PredicateExpression pieces rather than use the #Predicate macro (similar to building an NSPredicate from NSExpression). 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.

Fatbobman:

NSCompoundPredicate allows developers to combine multiple NSPredicate 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 to NSCompoundPredicate 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, utilizing PredicateExpression, under the current technical conditions.

[…]

The issue lies in the expression property being of the type any StandardPredicateExpression<Bool>, which doesn’t contain sufficient information to identify the specific PredicateExpression implementation type. Since Conjunction requires the exact types of the left and right sub-expressions for initialization, we are unable to use the expression 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 as build_Conjunction or build_Disjunction to generate new predicate expressions.

Jeremy Schonfeld:

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 then reduce 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 the closure and separate buildConjunction function, but this allows you to just append to conditions for each new property rather than needing to work with a combinatorial explosion of conditions using the macro.

Noah Kamara:

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.

Fatbobman:

This new strategy abandons the previous reliance on a custom StandardPredicateExpression implementation, opting instead for a type-casting strategy that effectively concretizes the information of expression. 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.

Helge Heß:

You know how Foundation (in part to support SwiftData) now has the Predicate macro? Well, RealityKit has its own generic QueryPredicate And guess what, they don’t need a macro to build them, looks like overloading the operators || and && is fine there.

Debbie Goldsmith:

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.

Andy Finnell:

Using generics to create SwiftData Predicates 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.

Jeremy Schonfeld:

Since Predicate is both Codable and Sendable, it requires that everything the predicate captures (i.e. all instances captured by the closure) are also Codable and Sendable.

Previously:

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.

Leave a Comment