KVO on Swift's computed properties
Asked Answered
W

3

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)

Westing answered 11/4, 2016 at 17:37 Comment(5)
The KVO is all about the setter, where is your setter @dhomes ?Pocky
Have a look at RxSwift.Mulloy
@Pocky that's the point! i'm referring can one observe a read-only computed property? would setting the width or height trigger a new area calculation? (would assume no, but maybe some fancy compiler ????)Westing
@dfri, quite interesting! thanks a lot!Westing
@dhomes take a look at Rob's answer it should be helpfulPocky
A
25

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])
Aeolis answered 11/4, 2016 at 17:49 Comment(3)
I believe according to those same documentation links, you are supposed to specify the context, no?Aloin
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.Aeolis
Good info! Thanks for sharing!Aloin
P
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

Pocky answered 11/4, 2016 at 18:20 Comment(0)
P
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
    }
}
Photon answered 8/2, 2021 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.