I have a simple class and I want to use keypath in the init
, something like this:
class V: UIView {
convenience init() {
self.init(frame: .zero)
self[keyPath: \.alpha] = 0.5
}
}
let v = View()
When I run this code I get a runtime error:
Fatal error: could not demangle keypath type from ' ����XD':
But, if I specify the type in keyPath it works fine:
class V: UIView {
convenience init() {
self.init(frame: .zero)
self[keyPath: \UIView.alpha] = 0.5
}
}
let v = View()
print(v.alpha) \\ prints 0.5
But, what's even stranger is that this code works:
class V: UIView {
convenience init() {
self.init(frame: .zero)
foo()
}
func foo() {
self[keyPath: \.alpha] = 0.5
}
}
let v = View()
print(v.alpha) \\ prints 0.5
What is the actual reason for this error?
Unsurprisingly, this is a compiler bug. In fact, it was reported only a couple weeks before you posted your question. The bug report contains a slightly simpler example that triggers the same crash:
class Foo: NSObject {
@objc let value: String = "test"
func test() {
let k1 = \Foo.value // Ok
let k2 = \Self.value // Fatal error: could not demangle keypath type from '�: file /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.8.25.8/swift/stdlib/public/core/KeyPath.swift, line 2623
}
}
Foo().test()
It turns out the Swift compiler was not properly handling key paths containing the covariant Self
type. In case you need a refresher, the covariant Self
type or dynamic Self
type allows you to specify that a method or property always returns the type of self
even if the class is subclassed. For example:
class Foo {
var invariant: Foo { return self }
var covariant: Self { return self }
}
class Bar: Foo {}
let a = Bar().invariant // a has compile-time type Foo
let b = Bar().covariant // b has compile-time type Bar
func walkInto(bar: Bar) {}
walkInto(bar: a) // error: cannot convert value of type 'Foo' to expected argument type 'Bar'
walkInto(bar: b) // works
But your example doesn't use dynamic Self
! Well actually it does: in the context of a convenience initializer, the type of self
is actually the dynamic Self
type, because a convenience initializer can also be called to initialize a subclass of your class V
.
So what exactly went wrong? Well, the Swift compiler did not include any logic to handle dynamic Self
when creating a key path. Under-the-hood, it essentially tried to emit a key path object of type ReferenceWritableKeyPath<Self, CGFloat>
. The type system doesn't allow you to use dynamic Self
in that context, and the runtime was not expecting it. The strange error message you received was the result of trying to decode this unexpected object type, which was encoded as a 4-byte relative pointer to the metadata for your V
class, followed by the suffix XD
indicating a dynamic Self
type (hence the error message containing 4 �
's followed by XD
). By playing around with different ways to create key paths involving dynamic Self
, I came across a number of different crashes at both compile-time and runtime.
I have submitted a fix for this bug. It turned out to be pretty simple: essentially, everywhere we find a dynamic Self
when creating a key path, we just replace it with the static self and add downcasts when necessary. Dynamic Self
only matters to enforce program correctness at compile time; it can and should be stripped out of the program before runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With