Reputation: 5779
Consider this class and the KeyPath
instances:
class Foo {
var name: String? = ""
}
var foo = Foo()
let keyPath: ReferenceWritableKeyPath<Foo, String?> = \.name
let keyPaths: [AnyKeyPath] = [keyPath]
We now have a collection of type-erased AnyKeyPath
that are really ReferenceWritableKeyPath
under the hood. (The rootType
and valueType
of the AnyKeyPath
are set.)
I want to convert the AnyKeyPath
back to a ReferenceWritableKeyPath
and use it to set foo.name = nil
. But the ergonomics of that are downright laughable. Here's what I've got:
func tryWrite<Base>(_ newValue: Any?, to: inout Base, through: AnyKeyPath, withKnownKeyPathValueType kpValueType: Any)
{
return _tryWrite(newValue, to: &to, through: through, withKnownKeyPathValueType: kpValueType)
}
func _tryWrite<Base, Value>(_ newValue: Any?, to: inout Base, through: AnyKeyPath, withKnownKeyPathValueType: Value)
{
guard let kp = through as? ReferenceWritableKeyPath<Base, Value> else {
print("failed to cast keypath")
return
}
to[keyPath: kp] = (newValue as! Value)
}
tryWrite(nil, to: &foo, through: keyPaths.first!, withKnownKeyPathValueType: Optional(""))
This works, but there HAS to be a better way. What is that better way?
This is a simplified example. In reality, I'm implementing "delete rules" in a data framework similar to SwiftData. You specify them the same way:
@Relationship(deleteRule: .nullify, inverse: \Bar.blah) var children: [Bar] = []
During a delete operation, I coalesce all the objects that need references nullified, which is how I end up with a collection of [AnyKeyPath]
that I need to turn back into ReferenceWritableKeyPath
.
Hence the need to make KeyPath
work rather than some other alternative, such as capturing closures.
Upvotes: 0
Views: 42
Reputation: 273540
Your own answer here works with optionals too.
It's just that you cannot directly pass nil
to the with:
parameter. You need to construct a nil
from the value type of the key path. Just like how you opened the existential Any
with an extra type parameter in _tryWrite
, you can open an existential metatype in the same way.
// this is helper function we use to open the existential
func typedNil<T: ExpressibleByNilLiteral>(of type: T.Type) -> T {
return nil
}
// this is same as your answer in the linked question
func _tryWrite<Base, Value>(to: inout Base, through: AnyKeyPath, with: Value) {
guard let kp = through as? ReferenceWritableKeyPath<Base, Value> else {
print("failed to cast keypath")
return
}
to[keyPath: kp] = with
}
func tryWriteNil<Base>(to: inout Base, through: AnyKeyPath) {
guard let valueType = type(of: through).valueType as? any ExpressibleByNilLiteral.Type else {
print("Key path value type is not optional!")
return
}
return _tryWrite(to: &to, through: through, with: typedNil(of: valueType))
}
Upvotes: 1