Swift Predicate: Incorrect(?) Fatal Error on KeyPath With Multiple Components

Context

I'm attempting to create a Swift Predicate using the #Predicate macro as shown here:

final class Foo 
{
    var title: String = ""
    var name: String = ""
}


let p = #Predicate<Foo> { $0.title == "test" }

This compiles fine, but at runtime crashes with this:

Foundation/KeyPath+Inspection.swift:65: Fatal error: Predicate does not support keypaths with multiple components

Here's what the macro expands to, according to Xcode:

Foundation.Predicate<Foo>({
    PredicateExpressions.build_Equal(
        lhs: PredicateExpressions.build_KeyPath(
            root: PredicateExpressions.build_Arg($0),
            keyPath: \.title
        ),
        rhs: PredicateExpressions.build_Arg("test")
    )
})

Question

Why? I've seen some discussion threads where this error message pops up when an optional is involved in a KeyPath, but that's not the case here. And there's not multiple components. This is essentially the simplest possible Predicate, lifted straight out of the example in the documentation, and it crashes at runtime. I'm a little mystified.

Details

  • Swift Language Version: 6
  • Xcode Version: 16.2
  • Deployment Target: macOS 15.2
  • This is a brand-new Xcode Project. I added the Foo class and then one single line in -applicationDidFinishLaunching() to construct the Predicate.
  • Stack trace:

NB:

I'm not sure if this is technically a "Swift Language" issue or a Foundation issue, but I've asked the question in more general places (SO) and gotten nothing. Swift Predicates are pretty new, the documentation is very sparse, and I'm hoping the Swift Forums have more expertise.

2 Likes

Don't know why, but this (class --> struct) doesn't crash:

struct Foo {
    var title: String = ""
    var name: String = ""
}

let p = #Predicate<Foo> { $0.title == "test" }
print ("-->", p)

Prints

--> capture1 (Swift.String): "test"
Predicate<AsyncStreamExplorer.C.(unknown context at $100005dec).(unknown context at $100005df8).Foo> { input1 in
    input1.title == capture1
}

Dear God, it's the final keyword.

You're correct: a struct worked just fine. So I started playing around with inheriting from NSObject, confirming to @Model (because I have other apps that use Predicates on SwiftData classes and those work just fine) and then finally, for no reason at all, I removed the final keyword. When I removed that, it worked. No other changes needed.

That has to be a bug, right? At the very least, if Predicates can't work with final classes, the error message being displayed (keypaths with multiple components) is entirely misleading.

2 Likes

I get the feeling that Predicate was really only tested in the context of SwiftData, because this works just fine:

@Model
final class Foo
{
    var title: String = ""
}

let p = #Predicate<Foo> { $0.title == "test" }   // Works

But this crashes:

final class Foo
{
    var title: String = ""
}

let p = #Predicate<Foo> { $0.title == "test" }   // Crashes

All @Model really does is rewrite the class into fancy computed properties, so I gave this a shot and found that it also works just fine:

final class Foo
{
    var title: String {
        return ""
    }
}

let p = #Predicate<Foo>{ $0.title == "test" }  // No problem.

The docs for Predicate make no mention about a failure to work with static dispatch. Apparently this is one more footgun to add to the long list of ways to hurt yourself with Predicate --> https://www.hackingwithswift.com/quick-start/swiftdata/how-to-filter-swiftdata-results-with-predicates

1 Like