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.
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))
}