Key Value Observing (KVO) with Swift Closures

This is an Swift class to allow KVO observing using Swift closures, useable from a Swift class that does not subclass NSObject.

From Swift, create a KeyValueObserver instance with the object being observed, the key path to observe and a closure to be called. As long as this instance remains alive, observations will be reported to the closure. To remove the observer, release the KeyValueObserver instance (so assign it to an optional so you can assign that to nil to release it).

let button = UIButton()

var kvo: KeyValueObserver? = KeyValueObserver(source: button, keyPath: "selected", options: .New) {
    (kvo, change) in
    NSLog("observing %@ %@", kvo.keyPath, change)
}

button.selected = true
button.selected = false
kvo = nil
button.selected = true

You can save the observer in an optional member and release it in the observation closure to implement a single-shot observation.

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!
    var kvo: KeyValueObserver?

    override func viewDidLoad() {
        super.viewDidLoad()
        kvo = KeyValueObserver(source: button, keyPath: "highlighted", options: .New) {
            (kvo, change) in
            NSLog("observing %@ %@", kvo.keyPath, change)
            self.kvo = nil
        }
    }
}

The implementation uses a global singleton NSObject subclass KVODispatcher dispatcher as the observer. KeyValueObserver instance is marshalled unretained into an UnsafeMutablePointer<KeyValueObserver> and passed as the context to addObserver:forKeyPath:options:context:.

KVODispatcher.observeValueForKeyPath() retrieves the KeyValueObserver instance from the context pointer and invokes the closure. Note that the KeyValueObserver is not retained when it is passed as the context or when it is extracted again - KeyValueObserver.deinit removes the observer so observeValueForKeyPath() should never be called with a deallocated instance. When you have finished observing, assign your KeyValueObserver optional to nil to remove the observer.

typealias KVObserver = (kvo: KeyValueObserver, change: [NSObject : AnyObject]) -> Void

class KeyValueObserver {
    let source: NSObject
    let keyPath: String
    private let observer: KVObserver

    init(source: NSObject, keyPath: String, options: NSKeyValueObservingOptions, observer: KVObserver) {
        self.source = source
        self.keyPath = keyPath
        self.observer = observer
        source.addObserver(defaultKVODispatcher, forKeyPath: keyPath, options: options, context: self.pointer)
    }

    func __conversion() -> UnsafeMutablePointer<KeyValueObserver> {
        return pointer
    }

    private lazy var pointer: UnsafeMutablePointer<KeyValueObserver> = {
        return UnsafeMutablePointer<KeyValueObserver>(Unmanaged<KeyValueObserver>.passUnretained(self).toOpaque())
    }()

    private class func fromPointer(pointer: UnsafeMutablePointer<KeyValueObserver>) -> KeyValueObserver {
        return Unmanaged<KeyValueObserver>.fromOpaque(COpaquePointer(pointer)).takeUnretainedValue()
    }

    class func observe(pointer: UnsafeMutablePointer<KeyValueObserver>, change: [NSObject : AnyObject]) {
        let kvo = fromPointer(pointer)
        kvo.observer(kvo: kvo, change: change)
    }

    deinit {
        source.removeObserver(defaultKVODispatcher, forKeyPath: keyPath, context: self.pointer)
    }
}


class KVODispatcher : NSObject {
    override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafeMutablePointer<()>) {
        KeyValueObserver.observe(UnsafeMutablePointer<KeyValueObserver>(context), change: change)
    }
}

private let defaultKVODispatcher = KVODispatcher()

A version of this as an Xcode playground is available on github.

Written on August 12, 2014