I've created a modern version of NSPredicateEditor that lets me construct complex searches of model objects. Each row becomes a Predicate and I then pass an array of these Predicates to a function that translates them into SQL++ database WHERE clauses.
BUT, the array of predicates that I pass magically vanishes. Here's the signature of the function I'm calling, which is generic on the type of model object being fetched:
At the site where I call this function, I can see a well-formed array of Foundation.Predicate. I have absolutely no idea what τ_0_0 is. There are no errors; the function runs just as if it had an empty array for predicates.
Can anyone point me in a direction to debug this? If I pass a single predicate from the array, that makes it through just fine.
Why Not Just Combine The Predicates?
Because the compiler falls over. With more than just a handful of the buildExpressions functions, the compiler can't type-check in reasonable time.
Limit the number of rows my PredicateEditor can have to, say, 10.
Use the ol' SwiftUI approach where we have 10 different versions of the function that currently takes an [Predicate<Foo>]. Each version has 1 to 10 different predicate parameters.
This works but it's also objectively awful.
Passing inside View works
Here's the View that contains my PredicateEditor:
struct MasterDatabaseSearchPane: View
{
@Environment(AppController.self) private var appController: AppController
@Binding var viewModel: MasterDatabaseWindowViewModel
@State private var beginValidation: Bool = false
var body: some View
{
VStack
{
PredicateEditor<MasterCue>(beginValidation: $beginValidation, searchAction: { predicates in
print("Start Search called with \(predicates.count) predicates: \(predicates)")
viewModel.liveCues = appController.modelController.mainContext?.liveResults(MasterCue.self, matching: .conjunction, ofPredicates: predicates, sortedBy: viewModel.cuesSortOrder.first ?? KeyPathComparator(\MasterCue.creationDate, order: .reverse))
})
Button("Search Master Cues") {
beginValidation = true
}
}
.padding()
}
}
PredicateEditor listens to beginValidation and, when it becomes true, the editor walks each row, turns that row's conditions into a Predicate<MasterCue> and then calls the searchAction closure with that array. The array of Predicates makes it to the closure just fine. But the ensuing call out to the method on modelController.mainContext happens and in that method I get the weird [Foundation.Predicate<Pack{τ_0_0}>] for the predicates parameter value.
Everything here is bound to the mainActor. I'm on Xcode 16.3 in Swift 6 language mode targeting macOS 14.6+
Unfortunately, I haven't been able to find the problem with Predicate. I was considering dropping the generics and writing a version that worked with just MasterCue (the type of model object I'm searching with predicates), but that would be a lot of surgery at this point and I'm not sure it would resolve whatever this is.
The Workaround
Instead, I realized I don't actually need to pass the array of Predicate<MasterCue> out of the View. All I do with them is convert them to a SQL++ WHERE clause and while I'd like to do that inside of my database abstraction layer because that's better encapsulation, I can just do it in the View itself.
So it ends up as:
PredicateEditor<MasterCue>(beginValidation: $beginValidation, searchAction: { predicates in
var clauses: [String] = []
for predicate: Predicate<MasterCue> in predicates
{
guard let clause: String = predicate.couchbaseQuery() else {
// Show alert that the search conditions could not be transformed to a SQL++ query
}
clauses.append(clause)
}
// Call appController.modelController.mainContext method with `clauses` instead of `predicates`.
})
Predicate has been quite a battle to use. Were I to do this again from scratch, I'd probably sacrifice the type-safe KeyPaths and use NSPredicate instead.