Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 889
Vince O'Sullivan Avatar asked Dec 04 '14 11:12

Vince O'Sullivan


2 Answers

Perhaps this is only available in swift 2+, but you can make an enum property directly observable without having to refer to its rawValue. It does come with some limitations however.

  1. have the containing class extend from NSObject (directly or indirectly)
  2. mark the enum with @objc
  3. extend the enum from Int
  4. declare the property as dynamic.
class SomeModel : NSObject {                          // (1) extend from NSObject
    @objc                                             // (2) mark enum with @objc
    enum ItemState : Int, CustomStringConvertible {   // (3) extend enum from Int
        case Ready, Set, Go

        // implementing CustomStringConvertible for example output
        var description : String {
            switch self {
            case .Ready: return "Ready"
            case .Set: return "Set"
            case .Go: return "Go"
            }
        }
    }

    dynamic var state = ItemState.Ready               // (4) declare property as dynamic
}

Elsewhere:

class EnumObserverExample : NSObject {
    private let _model : SomeModel

    init(model:SomeModel) {
        _model = model
        super.init()
        _model.addObserver(self, forKeyPath:"state", options: NSKeyValueObservingOptions.Initial, context: nil)
    }
    deinit {
        _model.removeObserver(self, forKeyPath:"state", context: nil)
    }

    override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if "state" == keyPath {
            print("Observed state change to \(_model.state)")
        }
    }
}

let model = SomeModel()
let observer = EnumObserverExample(model:model)
model.state = .Set
model.state = .Go

Outputs:

Observed state change to Ready    (because .Initial was specified)
Observed state change to Set
Observed state change to Go
like image 186
ɲeuroburɳ Avatar answered Sep 22 '22 10:09

ɲeuroburɳ


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
    }
}
like image 33
Mike Pollard Avatar answered Sep 20 '22 10:09

Mike Pollard