I am trying to get something very similar to the example in the WWDC 2017 Foundation talk working for KVO observing. The only differences that I see that are different from that talk are, I had to call super.init(), and I had to make the "kvo" token implicitly unwrapped.
The following is used in a playground:
struct Node {
let title: String
let leaf: Bool
var children: [String: Node] = [:]
}
let t = Node(title:"hello", leaf:false, children:[:])
let k1 = \Node.leaf
let k2 = \Node.children
t[keyPath: k1] // returns "false" works
t[keyPath: k2] // returns "[:]" works
@objcMembers class MyController : NSObject {
dynamic var tr: Node
var kvo : NSKeyValueObservation!
init(t: Node) {
tr = t
super.init()
kvo = observe(\.tr) { object, change in
print("\(object) \(change)")
}
}
}
let x = MyController(t: t)
x.tr = Node(title:"f", leaf:false, children:[:])
x
This error:
fatal error: Could not extract a String from KeyPath Swift.ReferenceWritableKeyPath<__lldb_expr_3.MyController, __lldb_expr_3.Node>: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.45.6/src/swift/stdlib/public/SDK/Foundation/NSObject.swift, line 85
Also, see this error:
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0). The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.
Is anyone else able to get something like this working, or is this a bug I need to report?
The bug here is that the compiler lets you say:
@objcMembers class MyController : NSObject {
dynamic var tr: Node
// ...
Node
is a struct
, so cannot be directly represented in Obj-C. However, the compiler still allows you to mark tr
as dynamic
– which requires @objc
. While @objcMembers
infers @objc
for members of the class, it only does so for members that are directly representable in Obj-C, which tr
is not.
So really, the compiler shouldn't let you mark tr
as dynamic
– I went ahead and filed a bug here, which has now been fixed and will be ready for Swift 5.
tr
needs to be @objc
& dynamic
for you to use KVO on it, because KVO requires method swizzling, which the Obj-C runtime provides, and Swift runtime doesn't. So to use KVO here you'll need to make Node
a class
, and inherit from NSObject
in order to expose tr
to Obj-C:
class Node : NSObject {
let title: String
let leaf: Bool
var children: [String: Node] = [:]
init(title: String, leaf: Bool, children: [String: Node]) {
self.title = title
self.leaf = leaf
self.children = children
}
}
(and if you take a look at the WWDC video again, you'll see the property they're observing is in fact of type a class
that inherits from NSObject
)
However, in the example you give, you don't really need KVO – you can just keep Node
as a struct
, and instead use a property observer:
struct Node {
let title: String
let leaf: Bool
var children: [String: Node] = [:]
}
class MyController : NSObject {
var tr: Node {
didSet {
print("didChange: \(tr)")
}
}
init(t: Node) {
tr = t
}
}
let x = MyController(t: Node(title:"hello", leaf:false, children: [:]))
x.tr = Node(title:"f", leaf: false, children: [:])
// didChange: Node(title: "f", leaf: false, children: [:])
And because Node
is a value type, didSet
will also trigger for any changes to its properties too:
x.tr.children["foo"] = Node(title: "bar", leaf: false, children: [:])
// didChange: Node(title: "f", leaf: false, children: [
// "foo": kvc_in_playground.Node(title: "bar", leaf: false, children: [:])
// ])
According to Apple, this is the intended behavior at this time as it depends on the Objective-C runtime. This was their response to my bug report and it further confirms what the accepted answer poster said.
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