NSNotificationCenter with Swift

Working with NSNotifciationCetner in Swift is fairly straightforward.

// This class will correctly work because it inherits from
// NSObject and thus automatically can be used by Objective-C.
// It does not use any Swift specific features.
//
class OkExample: NSObject {

    override init() {
        super.init()
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "handler:", name: "MyNotification", object: nil)
    }

    func handler(notif: NSNotification) {
        println("MyNotification was handled")
    }
}

Take for instance the example shown above. This class will work correctly with NSNotificationCenter because the class inherits from NSObject (you can also mark your class with the @objc).

We can easily call emit this notification by using notification center.

NSNotificationCenter.defaultCenter().postNotificationName("MyNotification", object: nil);  

That's all well and good, but what happens if your class uses Swift specific features. Well it won't work. Consider the following:

// This class will not work becuase it users the Swift
// specific feature of Generics. As a result, Objective-C will
// be unable to find the `handler` method resulting in a runtime exception
//
class BadExample<T>: NSObject {

    override init() {
        super.init()
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "handler:", name: "MyNotification", object: nil)
    }

    func handler(notif: NSNotification) {
        println("MyNotification was handled")
    }
}

In Swift and Objective-C in the Same Project it mentions a few caveats that have an impact on how selectors can be used from Objective-C code.

You’ll have access to anything within a class or protocol that’s marked with the @objc attribute as long as it’s compatible with Objective-C. This excludes Swift-only features such as:

  • Generics
  • Tuples
  • Enumerations defined in Swift
  • Structures defined in Swift
  • Top-level functions defined in Swift
  • Global variables defined in Swift
  • Typealiases defined in Swift
  • Swift-style variadics
  • Nested types
  • Curried functions

The end result, is that if you attempt to use NSNotificationCenter

2014-12-04 20:56:50.271 iOSExamples-ModelSync[4360:149860] -[_TtC21iOSExamples_ModelSync10BadExample00007F8DB3456710 handler:]: unrecognized selector sent to instance 0x7f8db3455360  
2014-12-04 20:56:50.281 iOSExamples-ModelSync[4360:149860] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_TtC21iOSExamples_ModelSync10BadExample00007F8DB3456710 handler:]: unrecognized selector sent to instance 0x7f8db3455360'  
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010eb69f35 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x00000001106adbb7 objc_exception_throw + 45
    2   CoreFoundation                      0x000000010eb7104d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
    3   CoreFoundation                      0x000000010eac927c ___forwarding___ + 988
    4   CoreFoundation                      0x000000010eac8e18 _CF_forwarding_prep_0 + 120
    5   CoreFoundation                      0x000000010eb39cec __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
    6   CoreFoundation                      0x000000010ea398a4 _CFXNotificationPost + 2484
    7   Foundation                          0x000000010ef426b8 -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
    8   iOSExamples-ModelSync               0x000000010e971d9e _TFC21iOSExamples_ModelSync11AppDelegate11applicationfS0_FTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVSs10DictionaryCSo8NSObjectPSs9AnyObject&#95;&#95;&#95;&#95;Sb + 1278
    9   iOSExamples-ModelSync               0x000000010e9720b0 _TToFC21iOSExamples_ModelSync11AppDelegate11applicationfS0_FTCSo13UIApplication29didFinishLaunchingWithOptionsGSqGVSs10DictionaryCSo8NSObjectPSs9AnyObject&#95;&#95;&#95;&#95;Sb + 560
    10  UIKit                               0x000000010f3f0475 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 234
    11  UIKit                               0x000000010f3f0fbc -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 2463
    12  UIKit                               0x000000010f3f3d2c -[UIApplication _runWithMainScene:transitionContext:completion:] + 1350
    13  UIKit                               0x000000010f3f2bf2 -[UIApplication workspaceDidEndTransaction:] + 179
    14  FrontBoardServices                  0x000000011223a2a3 __31-[FBSSerialQueue performAsync:]_block_invoke + 16
    15  CoreFoundation                      0x000000010ea9f53c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    16  CoreFoundation                      0x000000010ea95285 __CFRunLoopDoBlocks + 341
    17  CoreFoundation                      0x000000010ea95045 __CFRunLoopRun + 2389
    18  CoreFoundation                      0x000000010ea94486 CFRunLoopRunSpecific + 470
    19  UIKit                               0x000000010f3f2669 -[UIApplication _run] + 413
    20  UIKit                               0x000000010f3f5420 UIApplicationMain + 1282
    21  iOSExamples-ModelSync               0x000000010e97236e top_level_code + 78
    22  iOSExamples-ModelSync               0x000000010e9723aa main + 42
    23  libdyld.dylib                       0x0000000110e87145 start + 1
    24  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException  

The solution I came up with is to use a proxy class that will listen to NSNotificationCenter events and execute a closure upon observing that notification. The closure, becuase it is part of Swift can execute for any Swift class.

///
/// Provides proxy functionality for NSNotificationCenter
/// events for Swift classes that do not support integration
/// with Objective-C code.
///
@objc class ObserverProxy {

    var closure: (NSNotification) -> ();
    var name: String;
    var object: AnyObject?;

    init(name: String, closure: (NSNotification) -> ()) {
        self.closure = closure;
        self.name = name;

        self.start();
    }

    convenience init(name: String, object: AnyObject, closure: (NSNotification) -> ()) {
        self.init(name: name, closure);
        self.object = object;
    }

    deinit {
        stop()
    }

    func start() {
        NSNotificationCenter.defaultCenter().addObserver(self, selector:"handler:", name:name, object: object);
    }

    func stop() {
        NSNotificationCenter.defaultCenter().removeObserver(self);
    }

    func handler(notification: NSNotification) {
        closure(notification);
    }
}

Then, your Swift class can use this proxy class to listen for events.

// This class uses a proxy class that will listen for the notification.
// When the notification is fired, the proxy class will execute the supplied
// closure... which happens to be the handler on this class. This structure allows
// us to get around the limitation of Objective-C not having access to the methods
// in a Generic class
class ProxyExample<T>: NSObject {

    var myNotificationProxy: ObserverProxy?;

    override init() {
        super.init()
        myNotificationProxy = ObserverProxy(name: "MyNotification", closure: handler);
    }

    func handler(notif: NSNotification) {
        println("MyNotification was called")
    }
}

This technique could be modified to provide a cleaner syntax to allows more than a single closure to be applied to a single proxy. But this should help you get around limitations with Swift specific features preventing NSNotificationCenter from working.

comments powered by Disqus