Key Value Observation in Swift

With XCode Beta 5, this article is outdated. See updated article .

Documentation for KVO in Swift is listed as information forthcoming.

We favor KVO as a means for MVVM / reactive binding so we poked under the hood to see what it takes to support KVO with the current release of Swift.

Let’s start with defining a Publisher object with a property someProperty which we will observe from a Subscriber object.

import Foundation

class Publisher : NSObject {
  var someProperty = 100

  override var description: String { return "Publisher:{someProperty: (someProperty)}" }
}

As an aside, since we’re inheriting from NSObject overriding the description property gives us pretty printing abilities with println.

var pub = Publisher()
println("(pub)")

We’ll use an instance of the Subscriber class to listen for changes on Publisher.someProperty.

class Subscriber : NSObject  {
  override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: NSDictionary!, context: CMutableVoidPointer) {
    println("observeValueForKey: (object)")
  }
}

var sub = Subscriber()

pub.addObserver(sub, forKeyPath: "someProperty", options: nil, context: nil)

Ordinarily the call addObserver should succeed and our subscriber should start receiving messages from the publisher. Instead, the REPL spits out the following error:

objc[17599]: no class for metaclass 0x10286f410
Playground execution failed: error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.
* thread #1: tid = 0xa8623, 0x00007fff8569375b libobjc.A.dylib`_objc_trap(), queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
  * frame #0: 0x00007fff8569375b libobjc.A.dylib`_objc_trap()
    frame #1: 0x00007fff8569389b libobjc.A.dylib`_objc_fatal + 195
    frame #2: 0x00007fff8568c6a1 libobjc.A.dylib`_class_getNonMetaClass + 454
    frame #3: 0x00007fff8568d1f8 libobjc.A.dylib`_class_resolveMethod + 99
    frame #4: 0x00007fff856982c8 libobjc.A.dylib`lookUpImpOrForward + 286
    frame #5: 0x00007fff8568acb8 libobjc.A.dylib`class_getInstanceMethod + 52
    frame #6: 0x00007fff866bc277 Foundation`+[NSObject(NSKeyValueObservingCustomization) keyPathsForValuesAffectingValueForKey:] + 213
    frame #7: 0x00007fff866bbf21 Foundation`-[NSKeyValueUnnestedProperty _givenPropertiesBeingInitialized:getAffectingProperties:] + 141
    frame #8: 0x00007fff866bbc26 Foundation`-[NSKeyValueUnnestedProperty _initWithContainerClass:key:propertiesBeingInitialized:] + 145
    frame #9: 0x00007fff866bb7d9 Foundation`NSKeyValuePropertyForIsaAndKeyPathInner + 281
    frame #10: 0x00007fff866bb47b Foundation`NSKeyValuePropertyForIsaAndKeyPath + 169
    frame #11: 0x00007fff866e16d7 Foundation`-[NSObject(NSKeyValueObserverRegistration) addObserver:forKeyPath:options:context:] + 80

Apparently, even through our Publisher object inherits from NSObject, two class methods required for KVO are missing, namely keyPathsForValuesAffectingValueForKey and automaticallyNotifiesObserversForKey.

Let’s add these methods to Publisher with a default implementation:

extension Publisher {
  override class func keyPathsForValuesAffectingValueForKey(key: String!) -> NSSet! {
    return nil
  }

  override class func automaticallyNotifiesObserversForKey(key: String!) -> Bool {
    return true
  }
}

Now our subscriber receives change notifications from the publisher:

println("Changing value to 101")
pub.someProperty = 101

//Output
Changing value to 101
observeValueForKey: Publisher:{someProperty: 101}

Syntactic Sugar

With a little bit of syntactic sugar, we can clean up the subscription code to look like this:

pub.addSubscriber("someProperty") { println("callback: ($0), ($1)") }
println("Changing value to 102")
pub.someProperty = 102

//Output
Changing value to 102
callback: someProperty, Publisher:{someProperty: 102}

And the code:

typealias ObserverCallback = (String, AnyObject) -> ()

class SimpleSubscriber: NSObject {
  var callback: ObserverCallback

  init( cb: ObserverCallback ){
    self.callback = cb
  }

  override func observeValueForKeyPath(path: String!, ofObject object: AnyObject!, change: NSDictionary!, context: CMutableVoidPointer) {
    callback(path!, object!)
  }
}

extension Publisher {
  func addSubscriber(path: String, sub: NSObject) {
    self.addObserver(sub, forKeyPath: path, options: nil, context: nil)
  }

  func addSubscriber(path: String, sub: ObserverCallback ) {
    var wrapped = SimpleSubscriber(sub)
    self.addObserver(wrapped, forKeyPath: path, options: nil, context: nil)
  }
}

2 thoughts on “Key Value Observation in Swift

  1. great post! can you provide this as sample code? I’m trying to piece the various snippets above together and having trouble with closures syntax

    Like

  2. This slightly adapted version of your code works fine in a sample swift project (command line):


    import Foundation

    class Publisher : NSObject {
    var someProperty = 100
    func simpleDescription() -> String { return "Publisher:{someProperty: (someProperty)}" }
    }

    class Subscriber : NSObject {
    override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafePointer) {
    if let publisher = object as? Publisher {
    println("observed change to (keyPath) with value (publisher.someProperty)")
    }
    else {
    println("observing something that went wrong")
    }
    }
    }

    let sub = Subscriber()
    let pub = Publisher()

    println("(pub)")

    pub.addObserver(sub, forKeyPath: "someProperty", options: nil, context: nil)
    pub.someProperty = 500

    println("(pub)")

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s