Hi, I'm Alex Pretzlav

I write apps for humans, love to cook, and tinker with electronics. I live in Oakland, CA.

The Curious Case of NSObject.observe

16 February 2025

Key-Value Observing is one of the core technologies provided by Apple system frameworks like AppKit and UIKit. As described on the good documentation site:

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO, as it’s abbreviated, is built on top of the Objective-C runtime, and isn’t automatically available to Swift objects, but it’s still common to use KVO from Swift. To that end, Apple provides some Swift-specific APIs to make that easier, as documented at Using Key-Value Observing in Swift:

let v = UIView()
let observation = v.observe(\.frame) { view, change in
    print("new frame: \(view.frame)")
}
// Prints `new frame: (0.0, 0.0, 100.0, 100.0)`
v.frame.size = CGSize(width: 100, height: 100)

However, Apple’s documentation doesn’t link to API documentation for this observe method. As far as I can tell, none exists. It’s not documented anywhere on the main NSObject definition, nor in the KeyValueObserving protocol definition. In fact, if you option-click for “Quick Help” on the method in Xcode, you’re shown a single-line note about the return value, but no other documentation and notably no “Open in Developer Documentation” link like for other properties.

Screenshot of Xcode "Quick Help" pop-up for the observe method, the only documentation is the line "when the returned NSKeyValueObservation is deinited or invalidated, it will stop observing"

compared to

Screenshot of Xcode Quick Help for CGRect.size, which shows "A size that specifies the height and width of the rectangle." and a blue "Open in Developer Documentation" link

Using Xcode’s “Jump to Definition” for this method goes to the top of the generated Foundation definitions for Swift, but the method in question is nowhere to be found.

One of the most peculiar aspects of this method is that it seems at first impossible to implement yourself. Imagine a much simpler method that takes a KeyPath and returns its value:

extension NSObject {
    func get<T>(_ keyPath: KeyPath<Self, T>) -> T {
        return self[keyPath: keyPath]
    }
}

This will not compile; using Self as a generic constraint in a class extension isn’t allowed:

Covariant ‘Self’ or ‘Self?’ can only appear as the type of a property, subscript or method result; did you mean ‘NSObject’?

I’ve noticed this oddity with Realm’s observe(keyPaths:on:_:) method too, which always confused me because it requires an explicit class name on the keypath passed to an instance of that class:

dog.observe(keyPaths: [\Dog.adopted], { changes in

This Swift Forum thread was the magic explanation I was looking for and huge credit to Rob Mayoff for his hugely helpful answer: observe is implemented in a protocol extension, and it appears to be a private protocol, even though the method is public. Why is it implemented as a protocol extension? Rob explains:

 that is how you make the Self argument to the closure take on the lexical type of the observed object … rather than always being NSObject.

Rob even links to the Swift Foundation implementation that shows a version of the code, although it was removed in Swift 5.5:

public protocol _KeyValueCodingAndObserving {}
extension NSObject : _KeyValueCodingAndObserving {}

extension _KeyValueCodingAndObserving {
    
    ///when the returned NSKeyValueObservation is deinited or invalidated, it will stop observing
    public func observe<Value>(
            _ keyPath: KeyPath<Self, Value>,
            options: NSKeyValueObservingOptions = [],
            changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
        -> NSKeyValueObservation {

This is the key to this technique: the method taking a KeyPath must be defined in a protocol extension, which allows Self to refer to the static type of the instance at the time the method is called. Indeed, by moving over to a protocol extension, my get example works too:

protocol GetWithKeypath { }
extension NSObject: GetWithKeypath { }
extension GetWithKeypath {
    func get<T>(_ keyPath: KeyPath<Self, T>) -> T {
        return self[keyPath: keyPath]
    }
}

Realm could adopt this too, and could benefit from this technique for their “free” Combine support functions as well:

protocol RealmKeypathObserving { }
extension Object: RealmKeypathObserving { }

extension RealmKeypathObserving where Self: Object {
    func _observe(keyPaths: [PartialKeyPath<Self>],
                  on queue: DispatchQueue? = nil,
                  _ block: @escaping (ObjectChange<Self>) -> Void) -> NotificationToken {
        self.observe(keyPaths: keyPaths, on: queue, block)
    }

    func changesetPublisher(keyPaths: [PartialKeyPath<Self>] = []) -> RealmPublishers.ObjectChangeset<Self> {
        RealmSwift.changesetPublisher(self, keyPaths: keyPaths.map(_name(for:)))
    }
}

I’ll be keeping this technique in mind as I build new Swift APIs. It could definitely come in handy for metaprogramming-ish features.