I am trying to use KeyPath to set properties of a child class that inherits from a superclass. It works when a child class instance is passed explicitly but fails if superclass tries to use same KeyPath. Somehow Swift can't match type information from KeyPath against class instance, yet it's not obvious what's happening since all debug shows correct types being used. Below is a simple playground that illustrates the problem.
I can successfully set Child
properties when I pass it as an object via child.setValues(obj: child, from: newValues)
, not beautiful obviously.
But it fails if I use its Base
superclass method child.setValues(from: newValues)
— the switch
fails to match provided KeyPath. My debug print shows that class/object remains the same Child
(not Base
), the keyPath looks correct too... yet the switch logic seems to think it's something else:
Function: setValues(obj:from:) ...
__lldb_expr_27.Child
__lldb_expr_27.Child
__lldb_expr_27.Child
Mirror for Child
Function: setValues(obj:from:) [OK]
Function: setValues(from:) ...
__lldb_expr_27.Child
__lldb_expr_27.Child
__lldb_expr_27.Child
Mirror for Child
Function: setValues(obj:from:) ...
__lldb_expr_27.Child
__lldb_expr_27.Child
__lldb_expr_27.Child
Mirror for Child
Fatal error: Failed KeyPath: Swift.ReferenceWritableKeyPath<__lldb_expr_27.Child, Swift.Int> for type: __lldb_expr_27.Child: file KeyPath Generic.playground, line 56
I am puzzled as it seems that Swift erases the object's or KeyPath's type info somewhere yet I can't figure out what's happening. Any ideas?
Here's the full playground code:
import Foundation
class Base {
}
class Child: Base {
var name: String = ""
var number: Int = 0
enum CodingKeys: String, CodingKey, Hashable, CaseIterable, KeyPathMappable {
case name, number
var keyPath: PartialKeyPath<Child> {
switch self {
case .name: return \.name
case .number: return \.number
}
}
}
}
protocol KeyPathMappable {
associatedtype T
var keyPath: PartialKeyPath<T> { get }
}
extension Base {
func setValues<K>(from dict: [K : Any?]) where K: Hashable & KeyPathMappable {
print("Function: \(#function) ...")
debugPrint(Self.self)
debugPrint(self)
debugPrint(self.self)
print(Mirror(reflecting: self))
setValues(obj: self, from: dict)
}
func setValues<T, K>(obj: T, from dict: [K : Any?]) where K: Hashable & KeyPathMappable {
print("Function: \(#function) ...")
debugPrint(Self.self)
debugPrint(obj)
debugPrint(obj.self)
print(Mirror(reflecting: obj))
assert(obj.self is Self, "Object must be \(Self.self)")
for (ck, v) in dict {
let kp = ck.keyPath
switch kp {
case let p as ReferenceWritableKeyPath<T, String>:
obj[keyPath: p] = v as? String ?? String(describing: v)
case let p as ReferenceWritableKeyPath<T, Int>:
obj[keyPath: p] = v as? Int ?? 0
default:
assertionFailure("Failed KeyPath: \(kp) for type: \(self)")
}
}
print("Function: \(#function) [OK]")
}
}
let newValues: [Child.CodingKeys: Any] = [
.name: "Adam",
.number: 137,
]
let child = Child()
child.setValues(obj: child, from: newValues) // works
child.setValues(from: newValues) // case assertion fails