Context
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]
Question
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?
Background
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.
Context
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]
Question
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?
Background
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.
1 Answer
Reset to default 1Your 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))
}
nil
asValue
. You'd be building aReferenceWritableKeyPath<Foo, nil>
, which isn't a thing. – Bryan Commented Feb 1 at 9:40String?.none
. See also stackoverflow/q/79094440/5133585 – Sweeper Commented Feb 1 at 9:42AnyKeyPath
has all the information it needs (rootType
andvalueType
) but because you have to specialize the generics and can't just dosomeAnyKeyPath as! ReferenceWritableKeyPath<all-this-crap-is-known>
, I end up needing to initialize an instance of thevalueType
. Here's that's trivial (an empty string). In my real case, that initialization may not be trivial. So "better" means something more ergonomic and efficient. – Bryan Commented Feb 1 at 9:45