This is a simplified version of my code but I'd like to be able to initialise an NSPredicate
from a generic KeyPath<Root, Value>
but despite persistent attempts I've fallen short. Can anyone help? (solution needs to be iOS 15 compatible for my needs).
PS: I am aware I could alter the method to take a key: String
and have callers use #keypath(Object.property)
at the call-site but I do not want to do that, as I want to enforce a KeyPath is used for compile safety.
func makeNSPredicate<Root, Value>(keyPath: KeyPath<Root,Value>, value: Value) -> NSPredicate {
NSPredicate(format: "%K == '\(value)'", argumentArray: [keyPath])
}
Runtime error: signal SIGABRT
func makeNSPredicate<Root, Value>(keyPath: KeyPath<Root,Value>, value: Value) -> NSPredicate {
NSPredicate(format: "%K == '\(value)'", keyPath)
}
Compile error: Argument type 'KeyPath<Root, Value>' does not conform to expected type 'CVarArg'
func makeNSPredicate<Root, Value>(keyPath: KeyPath<Root,Value>, value: Value) -> NSPredicate {
NSPredicate(format: "%K == '\(value)'", #keyPath(keyPath))
}
Compile error: Argument of '#keyPath' refers to non-'@objc' property 'keyPath'
You can convert a Predicate
to NSPredicate
. This doesn't work with every Predicate
, but assuming your are only comparing key paths visible to Objective-C, and their values are also representable in Objective-C, this works. See the documentation for what kind of Predicate
s cannot be converted to NSPredicate
.
func makeNSPredicate<Root: NSObject, Value: Equatable & Codable>(keyPath: KeyPath<Root,Value>, value: Value) -> NSPredicate {
NSPredicate(Predicate<Root> { x in
PredicateExpressions.build_Equal(
lhs: PredicateExpressions.build_KeyPath(
root: PredicateExpressions.build_Arg(x),
keyPath: keyPath
),
rhs: PredicateExpressions.build_Arg(value)
)
})!
}
Though Predicate
is often used in a SwiftData context, you don't need to import SwiftData to use it. It is actually part of Foundation.
Note that Value
here needs to be Codable
, so you might have to implement Codable
for some of the types that you are comparing.
For lower iOS versions, you can get the key path string from a KeyPath
, and substitute that in with %K
. The downside is that you need a different format specifier for each type of value. You cannot use string interpolation here, like '\(value)'
. That won't work with numeric types.
Here are two examples - Int
and String
func makeNSPredicate<Root>(keyPath: KeyPath<Root, Int>, value: Int) -> NSPredicate {
NSPredicate(format: "%K == %lld", NSExpression(forKeyPath: keyPath).keyPath, value)
}
func makeNSPredicate<Root>(keyPath: KeyPath<Root, String>, value: String) -> NSPredicate {
NSPredicate(format: "%K == %@", NSExpression(forKeyPath: keyPath).keyPath, value)
}