Search code examples
swiftenumskey-value-observing

Swift KVO - Observing enum properties


I'm assembling a class which has several states, as defined by an enum, and a read-only property "state" which returns the instance's current state. I was hoping to use KVO techniques to observe changes in state but this doesn't seem possible:

dynamic var state:ItemState // Generates compile-time error: Property cannot be marked dynamic because its type cannot be represented in Objective-C

I guess I could represent each state as an Int or String, etc. but is there a simple alternative workaround that would preserve the type safety that the enum would otherwise provide?

Vince.


Solution

  • I came across the same problem a while ago. In the end I used an enum for the state and added an additional 'raw' property which is set by a property observer on the main state property.

    You can KVO the 'raw' property but then reference the real enum property when it changes.

    It's obviously a bit of a hack but for me it was better than ditching the enum altogether and losing all the benefits.

    eg.

    class Model : NSObject {
    
        enum AnEnumType : String {
            case STATE_A = "A"
            case STATE_B = "B"
        }
    
        dynamic private(set) var enumTypeStateRaw : String?
    
        var enumTypeState : AnEnumType? {
            didSet {
                enumTypeStateRaw = enumTypeState?.rawValue
            }
        }
    }
    

    ADDITIONAL:

    If you are writing the classes that are doing the observing in Swift here's a handy utility class to take some of the pain away. The benefits are:

    1. no need for your observer to subclass NSObject.
    2. observation callback code as a closure rather than having to implement observeValueForKeyPath:BlahBlah...
    3. no need to make sure you removeObserver, it's taken care of for you.

    The utility class is called KVOObserver and an example usage is:

    class ExampleObserver {
    
        let model : Model
        private var modelStateKvoObserver : KVOObserver?
    
        init(model : Model) {
    
            self.model = model
    
            modelStateKvoObserver = KVOObserver.observe(model, keyPath: "enumTypeStateRaw") { [unowned self] in
                println("new state = \(self.model.enumTypeState)")
            }
        }
    }
    

    Note [unowned self] in the capture list to avoid reference cycle.

    Here's KVOObserver...

    class KVOObserver: NSObject {
    
        private let callback: ()->Void
        private let observee: NSObject
        private let keyPath: String
    
        private init(observee: NSObject, keyPath : String, callback: ()->Void) {
            self.callback = callback
            self.observee = observee
            self.keyPath = keyPath;
        }
    
        deinit {
            println("KVOObserver deinit")
            observee.removeObserver(self, forKeyPath: keyPath)
        }
    
        override func observeValueForKeyPath(keyPath: String,
            ofObject object: AnyObject,
            change: [NSObject : AnyObject],
            context: UnsafeMutablePointer<()>) {
                println("KVOObserver: observeValueForKey: \(keyPath), \(object)")
                self.callback()
        }
    
        class func observe(object: NSObject, keyPath : String, callback: ()->Void) -> KVOObserver {
            let kvoObserver = KVOObserver(observee: object, keyPath: keyPath, callback: callback)
            object.addObserver(kvoObserver, forKeyPath: keyPath, options: NSKeyValueObservingOptions.New | NSKeyValueObservingOptions.Initial, context: nil)
            return kvoObserver
        }
    }