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: [:])
// ])