14

Is KVO on a computed property possible in Swift?

var width = 0
var height = 0

private var area : Double {
    get {
        return with * height
    }
}

self.addOberser(self, forKeyPath: "area", ......

Would a client code modifying the with or height trigger observeValueForKeyPath?

Just checking before engaging on a mayor class refactor. KVO's syntax being as annoying as it's is not worth even a playground if someone has an answer at hand. (I am assuming the answer is NO)

pkamb
  • 33,281
  • 23
  • 160
  • 191
David Homes
  • 2,725
  • 8
  • 33
  • 53

3 Answers3

24

That code won't work for two reasons:

  1. You must add the dynamic attribute to the area property, as described in the section “Key-Value Observing” under “Adopting Cocoa Design Patterns” in Using Swift with Cocoa and Objective-C.

  2. You must declare that area depends on width and height as described in “Registering Dependent Keys” in the Key-Value Observing Programming Guide. (This applies to Objective-C and Swift.) And for this to work, you also have to add dynamic to width and height.

    (You could instead call willChangeValueForKey and didChangeValueForKey whenever width or height changes, but it's usually easier to just implement keyPathsForValuesAffectingArea.)

Thus:

import Foundation

class MyObject: NSObject {

    @objc dynamic var width: Double = 0
    @objc dynamic var height: Double = 0

    @objc dynamic private var area: Double {
        return width * height
    }

    @objc class func keyPathsForValuesAffectingArea() -> Set<String> {
        return [ "width", "height" ]
    }

    func register() {
        self.addObserver(self, forKeyPath: "area", options: [ .old, .new ], context: nil)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("observed \(keyPath) \(change)")
    }
}

let object = MyObject()
object.register()
object.width = 20
object.height = 5

Output:

observed Optional("area") Optional([__C.NSKeyValueChangeKey(_rawValue: new): 0, __C.NSKeyValueChangeKey(_rawValue: kind): 1, __C.NSKeyValueChangeKey(_rawValue: old): 0])
observed Optional("area") Optional([__C.NSKeyValueChangeKey(_rawValue: new): 100, __C.NSKeyValueChangeKey(_rawValue: kind): 1, __C.NSKeyValueChangeKey(_rawValue: old): 0])
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    I believe according to those same documentation links, you are supposed to specify the context, no? – Mark A. Donohoe May 02 '17 at 15:16
  • 3
    Usually it's a good idea to use a `context` for several reasons, but this answer wasn't about that part of KVO. Also, if you only inherit from `NSObject`, you don't need to use a `context` unless you want to, because `NSObject` doesn't register for any KVO notifications, so there's no possibility of conflicting with your superclass. – rob mayoff May 02 '17 at 15:31
  • Good info! Thanks for sharing! – Mark A. Donohoe May 02 '17 at 21:24
2

As @Rob stated in his answer, make area dynamic to be observed from objective-c

Now add willSet { } and didSet { } for width and height properties,
inside willSet for both properties add this self.willChangeValueForKey("area") and in didSet add self.didChangeValueForKey("area");

Now observers of area will be notified every time width or height change.

Note: this code is not tested, but I think it should do what expected

Basheer_CAD
  • 4,908
  • 24
  • 36
2

What I usually do these days to avoid willChangeValueForKey and didChangeValueForKey with unchecked string property names is make the computed property private(set) and create a private func to update it.

import Foundation

class Foo: NSObject {

    @objc dynamic private(set) var area: Double = 0
    @objc dynamic var width: Double = 0 { didSet { updateArea() } }
    @objc dynamic var height: Double = 0 { didSet { updateArea() } }

    private func updateArea() {
        area = width * height
    }
}