16

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?

possen
  • 8,596
  • 2
  • 39
  • 48

3 Answers3

23

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: [:])
// ])
Hamish
  • 78,605
  • 19
  • 187
  • 280
1

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.

possen
  • 8,596
  • 2
  • 39
  • 48
0

The accepted answer is right. But I want to say what I know about KVO in Swift.

Swift also mixing compile OC in many Kit and implementation, such as KVO. So, you should know how KVO was implemented in OC.

When you addObserver: forKeyPath: for a object, The OC runtime create a subclass inherit the class that the object belongs to, and then rewrite the setter method for the object, when object changed, it call setter and setValue(_ value: Any?, forKey key: String) to notify the change.

Now, let's back to Swift, So your keyPath should be a OC accepted type.

class A {  // it's a Swift class but not a OC class inherit from NSObject
   var observation: NSKeyValueObservation?
   @objc dynamic var count: Int = 0  // @objc for OC, dynamic for setter
}

observation = observe(\.count, options: [.new, .old]) { (vc, change) in
   print("new: \(change.newValue), old: \(change.oldValue)")
}  // it's very strange when don't use result, the observe is failure.

Above what I know about KVO, and I will search it and update my answer continuously.

yuanjilee
  • 429
  • 5
  • 16