Modern Notification Handling in Swift 2

Handling a notification in Swift is not as concise as writing an optional chain or other things that take the advantage of Swift’s advanced features because the notification system was built upon Objective-C technology and using a userInfo dictionary to carry informations.

Such as handling a UIKeyboardWillShowNotification, you probably will write the code likes this:

func handleNotification(notification: NSNotification) {  
    guard notification.name == UIKeyboardWillShowNotification 
        else { return }

    guard let initialFrame = notification
        .userInfo?[UIKeyboardFrameBeginUserInfoKey] as? CGRect
        else { return }

    guard let finalFrame = notification
        .userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect
        else { return }

    print("Initial frame: \(initialFrame)")
    print("Final frame: \(finalFrame)")
}

which introduced many guarding expressions.

Actually, if you are a developer with dream, you probably want to write the code above like this:

func handleNotification(notification: NSNotification) {  
    switch notification.name {
    case UIKeyboardWillShowNotification:
        let initialFrame = notification.initialFrame
        let finalFrame = notification.finalFrame
        print("Initial frame: \(initialFrame)")
        print("Final frame: \(finalFrame)")
    default: return
    }
}

But that's impossible. Since the switch-case syntax above only matches the name of notification but not the type, you cannot pull the initialFrame and finalFrame out of the air.

A Perhaps Solution: Subclassing NSNotification

Perhaps, there is one way to make notification handling concise - subclassing NSNotification. With a subclass, we can easily get the information by accessing the getters of the NSNotification subclass instance.

Here is an ideal example:

class CustomNotification: NSNotification {  
    static let name = "CustomNotification"

    let notifyTiming: NSTimeInterval = NSDate.timeIntervalSinceReferenceDate()
    let notifyContent: String

    private var _name: String
    override var name: String { return _name }

    private var _userInfo: [NSObject : AnyObject]?
    override var userInfo: [NSObject : AnyObject]? { return _userInfo }

    private var _object: AnyObject?
    override var object: AnyObject? { return _object }

    init(object: AnyObject?, content: String) {
        _name = self.dynamicType.name
        _object = object
        notifyContent = content
        super.init(name: _name, object: object, userInfo: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        _name = self.dynamicType.name
        notifyContent = ""
        super.init(coder: aDecoder)
    }
}

But you can quickly find this is not the solution because you always fail to instantiate the CustomNotification by receiving an error message likes:

*** initialization method -initWithName:object:userInfo: cannot be sent to an abstract object of class Untitled.CustomNotification: Create a concrete instance!

As Apple wrote a note in NSNotification's class reference:

NSNotification is a class cluster with no instance variables. As such, you must subclass NSNotification and override the primitive methods name, object, and userInfo. You can choose any designated initializer you like, but be sure that your initializer does not call [super init]. NSNotification is not meant to be instantiated directly, and its init method raises an exception.

and in NSNotification's Swift header file:

convenience init() /*NS_UNAVAILABLE*/ /* do not invoke; not a valid initializer for this class */  

you probably should not call any initializer of NSNotification in your subclass' implementation. And those constraints make subclassing NSNotification impossible.

A Certain Solution: Build Your Own Notification Broadcasting System

How about building a native Swift notification broadcasting system? That must be a certain right solution because we take the whole control of the system implementation this time.

Since we are mimicking the mechanism of NSNotificationCenter and its relative infrastructures, it would be better that we firstly figure out how those NS stuffs work.

How Does Cocoa/CocoaTouch Notification Broadcasting System Work

There are 3 components in Cocoa/CocoaTouch notification broadcasting system: NSNotificationCenter, NSNotificationQueue and NSNotification. Most of us probably only know about NSNotificationCenter and NSNotification. But in fact, NSNotificationQueue is the real power of the Cocoa/CocoaTouch notification broadcasting system which offers asynchronous posting and notification coalescing.

I think most people are more perceptive to pictures but not texts, so I drew a picture to briefly explain how NSNotificationCenter and its relative infrastructures construct and work.

IMG

As the picture shows:

  • Each process has its only one NSNotificationCenter.
  • Each thread has a default NSNotificationQueue.
  • You can create your custom NSNotificationQueue and attach it to a thread.
  • The notifications enqueued to NSNotificationQueue with postingStyle set to PostNow will be posted immediately.
  • The notifications enqueued to NSNotificationQueue with postingStyle set to PostASAP will be posted in the end of current run loop iteration.
  • The notifications enqueued to NSNotificationQueue with postingStyle set to PostWhenIdle will be posted when the run loop is idle.

But there were still some details not drew in the picture.

  • The first is notification coalescing. For each NSNotification enqueued to NSNotificationQueue, you can make the queue to coalesce them on its name or object or just not to coalesce. When PostASAP or PostWhenIdle notifications are about to be posted, the notification queue will coalesce them on its name or object, which can reduce system resource consuming by avoiding intensive notification posting.

  • The second is NSNotificationQueue utilizes NSNotificationCenter's postNotification() function to post notification.

Pros and Cons in Cocoa/CocoaTouch Notification Broadcasting System

Obviously, Cocoa/CocoaTouch notification broadcasting system comes with such pros:

  • Asynchronous notification posting.
  • Notification coalescing.
  • Decouples message sender and receiver.

But due to the API design, the system also comes with such cons:

  • As the NSNotificationCenter maintains a strong relationship between itself and the notification observers, you should remember to remove the notification observer before you intent to end its life-cycle.
  • Use a dictionary to carry information which lose the explicit type info.
  • Notification is identified by a string, which might be unintentionally duplicate sometime.

And those cons are what you are going to solve.

New Design

As I mentioned before, a developer with dream must write a notification handling code like this:

func handleNotification(notification: NSNotification) {  
    switch notification.name {
    case UIKeyboardWillShowNotification:
        let initialFrame = notification.initialFrame
        let finalFrame = notification.finalFrame
        print("Initial frame: \(initialFrame)")
        print("Final frame: \(finalFrame)")
    default: return
    }
}

But for the switch-case syntax above only matches the name of the notification but not the type, we can't pull the initialFrame and finalFrame out of the air.

Wait! How about just making the switch-case syntax matches the type of notification? This time we design the system, and we can make the notification identified by its type. So we can image that we write the code above like this:

func handleNotification(notification: NotificationType) {  
    switch notification {
    case let keyboardWillShowNotification as UIKeyboardWillShowNotification:
        let initialFrame = keyboardWillShowNotification.initialFrame
        let finalFrame = keyboardWillShowNotification.finalFrame
        print("Initial frame: \(initialFrame)")
        print("Final frame: \(finalFrame)")
    default: return
    }
}

That's fantastic. The NotificationType can be a protocol and we can handle it by converting it to a concrete type via Swift's native pattern matching feature. The initialFrame and finalFrame is not pulled out of the air this time, they are pulled out of a concrete instance!

So here is one requirement:

Notification is identified by its type.

Are there any more requirements? Yes, as I mentioned before, NSNotificationCenter maintains a strong relationship between itself and any notification observer, which overloads the developer's brain by keeping them remembering to call removeObserver() series function at the right time.

Notification center maintains a weak relationship between itself and the notification subscriber.

And we can do our actual design now.

NotificationType

As a protocol with type aliases declared cannot be used in a generic container such as Array, Dictionary or Set, we must define a primitive notification type without any type aliases declared to make it manageable.

public protocol PrimitiveNotificationType {  
    public var notificationName: String
}
public protocol NotificationType: PrimitiveNotificationType {  
    typealias NotificationPoster: NotificationPosterType
    var notificationPoster: NotificationPoster {get}
}

NotificationPosterType

NotificationPosterType is designed to offer convenience to post notifications. You might notice that NotificationType's notificationPoster property is typed as NotificationPosterType, and that is a constraint to keep other types from being a notification poster, which ensuring the code safety.

public protocol NotificationPosterType: class {  
    public func postNotification
        <N: NotificationType where N.NotificationPoster == Self>
        (notification: N)
}

NotificationSubscriberType

The NotificationSubscriberType is designed to handle a notification and offer convenience to subscribe notifications.

public protocol NotificationSubscriberType: class {  
    func subscribeNotificationOfType
        <N: NotificationType>
        (notificationType: N.Type,
        onQueue queue: NotificationQueue)

    func handleNotification(notification: PrimitiveNotificationType)
}

NotificationCenter

The notification center a very simple subscribing and posting machine. We can get a process unique notification center via accessing the shared property of NotificationCenter.

public class NotificationCenter {  
    public static let shared: NotificationCenter

    public func subscriber
        <N: NotificationType>
        (subscriber: NotificationSubscriberType,
        subscribeNotificationOfType notificationType: N.Type,
        onQueue queue: NotificationQueue)

    public func postNotification(notification: PrimitiveNotificationType,
        onQueue queue: NotificationQueue)
}

NotificationQueue

NotificationQueue offers the ability to coalesce and asynchronously post notifications.

public class NotificationQueue {  
    public enum PostTiming: Int {
        case WhenIdle
        case ASAP
        case Now
    }

    public struct Coalescing: OptionSetType {
        public let rawValue: UInt
        public init(rawValue: UInt) { self.rawValue = rawValue }

        public static let OnType    = Coalescing(rawValue: 1 << 0)
        public static let OnPoster  = Coalescing(rawValue: 1 << 1)
    }

    public class var current: NotificationQueue

    public func enqueueNotification
        <N: NotificationType>
        (notification: N,
        timing: PostTiming, 
        coalesce: Coalescing,
        forModes modes: NSRunLoopMode)

    public func dequeueNotificationsMatching
        <N: NotificationType>
        (notification: N,
        coalesce: Coalescing)
}

Implementations

I think much of details in the implementation is very easy to be done and you guys can check them out in the source code which I will post in the last of this article. But there are still some key points should be pointed out.

When to Post: ASAP and WhenIdle

To determine when to post, we must figure out how to get the run loop events. As the NSRunLoop is only able to add timer and ports which act as input sources, there is no object-oriented way to observe run loop events. But we can do this with CoreFoundation.

public class NotificationQueue {

    ......

    private init(_ notificationCenter: NotificationCenter) {
        self.notificationCenter = notificationCenter
        self.runLoopObserver = CFRunLoopObserverCreateWithHandler(
            kCFAllocatorDefault,
            CFRunLoopActivity.AllActivities.rawValue,
            true,
            0) { (observer, activity) -> Void in
                let rawRunloopMode =
                    CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent())
                let runLoopMode =
                    NSRunLoopMode(rawValue: rawRunloopMode as String)

                if activity.contains(.BeforeWaiting) {
                    // Post ASAP notifications here
                }
                if activity.contains(.Exit) {
                    // Post When-Idle notification here
                }
        }
        CFRunLoopAddObserver(CFRunLoopGetCurrent(),
            runLoopObserver,
            kCFRunLoopCommonModes)
    }

    deinit {
        CFRunLoopRemoveObserver(CFRunLoopGetCurrent(),
            runLoopObserver,
            kCFRunLoopCommonModes)
    }

    ......
}

How to maintain a weak relationship between the NotificationCenter and a notification subscriber

I did this by defining such a struct:

public struct Weak<T: AnyObject>: Hashable {  
    private weak var _value: T?
    public weak var value: T? { return _value }
    public init(_ aValue: T) { _value = aValue }

    public var hashValue: Int {
        guard let value = self.value else { return 0 }
        return ObjectIdentifier(value).hashValue
    }
}

public func ==<T: AnyObject>(lhs: Weak<T>, rhs: Weak<T>) -> Bool {  
    return lhs.value === rhs.value
}

You can see this struct maintains a weak relationship to an object.

Source Code

This notification system is included in my personal used framework: Nest.

And here is the link on GitHub: Nest

WeZZard

Independent iOS developer, World of Warcraft add-on developer. Interested in Computer Graphics and Machine Learning.

People's Republic of China https://github.com/WeZZard