Key value observation in Swift

For the new project I need to use KVO, so I'm now refreshing my memory on how KVO works (with Swift specifically) and doing some experiments. Here's what I found with some questions inline. Full test code is below.

  1. observing the older (not type-safe) way works:
    kvo.addObserver(self, forKeyPath: "property", options: [], context: nil)
    ...
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("got a change: \(change)")
    }
  1. contrary to the documentation observing via the key path of the form: \.objectToObserve.property, i.e.:
    observation = self.observe(\.objectToObserve.property, ...)

doesn't work and fails at runtime with:

    Could not extract a String from KeyPath \Tester.kvo.instanceProperty

Should it work, or is it a bug in documentation?

  1. I was able to work around this issue by changing observation to:
    observation = objectToObserve.observe(\.property, ...)
  1. Attempting observing a static property doesn't work:
    TypeToObserve.addObserver(self, forKeyPath: "staticProperty", options: [], context: nil)

No errors, compile or runtime, just nothing happens. Is it supposed to work at all?
There are quite a few class var properties commented as KVO observable in AVFoundation for example.

  1. If it supposed to work, is there a better type safe swift way of observing static properties?

Full test code
import Foundation

@objc class KVO: NSObject {
    @objc dynamic static var staticProperty = 0
    @objc dynamic var instanceProperty = 0
}

class Tester: NSObject {
    private var observation: NSKeyValueObservation?
    
    init(kvo: KVO) {
        super.init()
        
        // uncomment the relevant fragment:
        
        // 1. observe instance property "old way", works:
        // kvo.addObserver(self, forKeyPath: "instanceProperty", options: [.old, .new], context: nil)

        // 2. observe instance property "new documented way", traps at runtime:
        // 🛑 Could not extract a String from KeyPath \Tester.kvo.instanceProperty
        // self.observation = observe(\.kvo.instanceProperty, options: [.old, .new]) { object, change in
        //    print("got a change (new documented way): \(change)")
        // }
        
        // 3. observe instance property "new better way", works:
        // self.observation = kvo.observe(\.instanceProperty, options: [.old, .new]) { object, change in
        //    print("got a change (new better way): \(change)")
        // }
        
        // 4. observe static property - doesn't work, nothing happens:
        // KVO.addObserver(self, forKeyPath: "staticProperty", options: [.old, .new], context: nil)
    }
    
    // observe instance property the old way support
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("got a change (old way): \(change)")
    }
}

let kvo = KVO()
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { timer in
    kvo.instanceProperty += 1
    KVO.staticProperty += 1
}
let tester = Tester(kvo: kvo)
RunLoop.main.run(until: .distantFuture)
1 Like

I assume KVO relies on class var (instead of static var), as it has to dynamically subclass and override the setter to work.

1 Like

Thank you, this works!

    AVCaptureDevice.addObserver(self, forKeyPath: "centerStageControlMode", options: [.old, .new], context: nil)

Is it possible to use a type safe key-path version here?