Notification Center on steroids

Photo by Raagesh C / Unsplash

Like all Objective-C APIs, Notification Center has Strings everywhere. With iOS10 Apple introduced NSNotification.Name but, still, there is some space for improvement.

If you use notifications without any payload, new nested type is enough. Problem starts with sending additional data with your message. For this purpose, Apple gives userInfo dictionary. Its type — [AnyHashable: Any], makes it very difficult to maintain. Sender and receiver have to know the key value and type, remember what the value type is. Receiver always has to cast from Any. For me, this is less than an optimal way to go.

To solve the problem, I'll introduce strongly typed Notification Center extension, starting with Payload.

/// Protocol for payload. Provide unique `name`.
public protocol NotificationPayload {
    static var name: Notification.Name { get }
}

Payload contains just required name for sending a notification. In your specific implementation, you can create a struct/class with additional properties and send everything you want.

Second part of the setup is generic wrapper for payload. I'll use struct for that, it ensures immutability and thread safety.

/**
 Struct holding notification payload.
 
 Create instance of `NotificationItem` with Your own payload
 */
public struct NotificationItem<T: NotificationPayload> {
    let payload: T
    
    public init(_ payload: T) {
        self.payload = payload
    }
    var name: Notification.Name {
        return T.name
    }
}

OK, the foundation is done. What's left is sending and receiving notifications. Our code - NotificationCenter extension, will handle sending and receiving notifications. Extension for Notification will make decoding and retrieving payload easy and clean.

First things first, decoding:

let payloadKey = "FancyNotificatonCenter_payload_userInfo_key"

public extension Notification {
    
    /// Decodes `Notification` to provide payload
    ///
    /// - Returns: retrieved `payload` or nil
    func decode<T: NotificationPayload>() -> T? {
        return userInfo?[payloadKey] as? T
    }
}

Notification will decode itself and return strongly typed payload. Lastly, sending and receiving:

public extension NotificationCenter {
    /// Creates a notification with a given item and posts it to the notification center.
    ///
    /// - Parameter notificationItem: `NotificationItem` to be posted
    func post<T: NotificationPayload>(_ notificationItem: NotificationItem<T>) {
        post(name: notificationItem.name,
             object: nil,
             userInfo: [payloadKey : notificationItem.payload])
    }
    
    func addObserver(_ observer: NSObject, 
                     selector aSelector: Selector, 
                     for payload: NotificationPayload.Type ) {
        addObserver(observer, selector: aSelector, name: payload.name, object: nil)
    }
}

Easy and simple, ready to go. You can post and observe notifications using strongly typed API. No more names, keys, casting and unmaintainable code.


But wait, there is more!

Although notification code looks better now, I don't feel it is Swifty enough. We can spice it up a notch.

First flavour, fancy sending.

infix operator <-

/// Convinient operator for `post(notificationItem)`
public func <-<T: NotificationPayload>(lhs: NotificationCenter, rhs: NotificationItem<T>) {
    lhs.post(rhs)
}

By adding new sending operator <-, there is no need to call a function on notification center, like a cave person would do! For me, it looks much more readable.

NotificationCenter.default <- payload

Second flavour, operator for decoding:

prefix operator <<

/// Convinient operator for `decode`.
///
/// - Parameter lhs: `Notification` to decode
/// - Returns: decoded `NotificationPayload`
public prefix func <<<T: NotificationPayload>(lhs: Notification) -> T? {
    return lhs.decode()
}

By using <<, decoding looks awesome: let payload: T?= <<notification.

Lastly, we can add one more extension for NotificationCenter - support for closures, it will glue everything together:

public extension NotificationCenter {
    /// Adds an entry to the notification center to receive notifications that passed to the provided block.
    /// - Parameters:
    ///   - obj: The object that sends notifications to the observer block. Specify a sender to deliver only notifications from this sender.
    ///   When nil, the notification center doesn’t use the sender as criteria for the delivery.
    ///   - queue: The operation queue where the block runs.
    ///   When nil, the block runs synchronously on the posting thread.
    ///   - block: The block that executes when receiving a notification.
    ///   The notification center copies the block. The notification center strongly holds the copied block until you remove the observer registration.
    ///   The block takes one argument: the notification.
    func addObserver<T: NotificationPayload>(
                     object obj: Any?,
                     queue: OperationQueue?,
                     using block: @escaping (T?) -> Void) -> NSObjectProtocol {
        addObserver(forName: T.name, 
                    object: obj, 
                    queue: queue) { notification in
            let payload: T? = <<notification
            block(payload)
        }
        
    }
}

If you like my idea, use it. Available here as SPM.

Artur Gruchała

Artur Gruchała

I started learning iOS development when Swift was introduced. Since then I've tried Xamarin, Flutter, and React Native. Nothing is better than native code:)
Poland