Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

KVO observation not working with Swift generics

If I observe a property using KVO, if the observer is a generic class then I receive the following error:

An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

The following setup demonstrates the problem succinctly. Define some simple classes:

var context = "SomeContextString"

class Publisher : NSObject {
    dynamic var observeMeString:String = "Initially this value"
}

class Subscriber<T> : NSObject {
    override func observeValueForKeyPath(keyPath: String,
                    ofObject object: AnyObject,
                    change: [NSObject : AnyObject],
                    context: UnsafeMutablePointer<Void>) {
        println("Hey I saw something change")
    }
}

Instantiate them and try to observe the publisher with the subscriber, like so (done here inside a UIViewController subclass of a blank project):

var pub = Publisher()
var sub = Subscriber<String>()

override func viewDidLoad() {
    super.viewDidLoad()

    pub.addObserver(sub, forKeyPath: "observeMeString", options: .New, context: &context)
    pub.observeMeString = "Now this value"
}

If I remove the generic type T from the class definition then everything works fine, but otherwise I get the "received but not handled error". Am I missing something obvious here? Is there something else I need to do, or are generics not supposed to work with KVO?

like image 320
chris838 Avatar asked Nov 23 '14 10:11

chris838


1 Answers

Explanation

There are two reasons, in general, that can prevent a particular Swift class or method from being used in Objective-C.

The first is that a pure Swift class uses C++-style vtable dispatch, which is not understood by Objective-C. This can be overcome in most cases by using the dynamic keyword, as you obviously understand.

The second is that as soon as generics are introduced, Objective-C looses the ability to see any methods of the generic class until it reaches a point in the inheritance hierarchy where an ancestor is not generic. This includes new methods introduced as well as overrides.

class Watusi : NSObject {
    dynamic func watusi() {
        println("watusi")
    }
}

class Nguni<T> : Watusi {
    override func watusi() {
       println("nguni")
    }
}

var nguni = Nguni<Int>();

When passed to Objective-C, it regards our nguni variable effectively as an instance of Watusi, not an instance of Nguni<Int>, which it does not understand at all. Passed an nguni, Objective-C will print "watusi" (instead of "nguni") when the watusi method is called. (I say "effectively" because if you try this and print the name of the class in Obj-C, it shows _TtC7Divided5Nguni00007FB5E2419A20, where Divided is the name of my Swift module. So ObjC is certainly "aware" that this is not a Watusi.)

Workaround

A workaround is to use a thunk that hides the generic type parameter. My implementation differs from yours in that the generic parameter represents the class being observed, not the type of the key. This should be regarded as one step above pseudocode and is not well fleshed out (or well thought out) beyond what's needed to get you the gist. (However, I did test it.)

class Subscriber : NSObject {
    private let _observe : (String, AnyObject, [NSObject: AnyObject], UnsafeMutablePointer<Void>) -> Void

    required init<T: NSObject>(obj: T, observe: ((T, String) -> Void)) {
        _observe = { keyPath, obj, changes, context in
            observe(obj as T, keyPath)
        }
    }
    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
        _observe(keyPath, object, change, context)
    }
}

class Publisher: NSObject {
    dynamic var string: String = nil
}

let publisher = Publisher()
let subscriber = Subscriber(publisher) { _, _ in println("Something changed!") }
publisher.addObserver(subscriber, forKeyPath: "string", options: .New, context: nil)
publisher.string = "Something else!"

This works because Subscriber itself is not generic, only its init method. Closures are used to "hide" the generic type parameter from Objective-C.

like image 70
Gregory Higley Avatar answered Nov 14 '22 22:11

Gregory Higley