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.