Typed Notifications

Episode #410 | 17 minutes | published on September 27, 2019 | Uses Swift-5.0, Xcode-11.0
Subscribers Only
Sending notifications in your apps is a great way to signal events without having tight coupling between the interested parties. However, in practice this means creating notifications using string names, and passing data as an untyped dictionary means there is opportunity for misspellings and misunderstandings in what data will be passed for which notifications. This can lead to bugs and crashes. In this episode we'll look at creating a more Swift-friendly type-safe wrapper around notifications.

Sending Notifications with NotificationCenter

We will start with the basic usage of NotificationCenter.

Each notification has a name you want to listen for and is of type Notification.Name. There are two different ways we can subscribe to notification. One way is by using block we will create a strong reference to the observer calling .addObserver and pass the name and the object whose notifications the observer wants to receive. If you pass the object as nil, the notification center does not use a notification’s sender to decide whether to deliver it to the observer.

The second method is by using a selector-based syntax. We specify the object registered as an observer and the selector that refers to a function marked with @objc. Along with the selector, we also pass the name of the notification for which to register the observer and the object whose notifications the observer wants to receive. The problem with the selector-based syntax is, we cannot pass this subscription if we are using types that Objective-C does not understand, such as Swift structs or classes that use generics.

To post notifications, we call post with the notification name, the object sending the notification and a userInfo dictionary.

To interpret the notification data we need to ensure that the userInfo dictionary is assigned the right set of a key-value pairs. This is just a bag of data, so when you receive it on the subscribing end, you'll need to safely cast the values to the types you expect, and this is error prone and tedious.

extension Notification.Name {
    static var subscriptionChanged = Notification.Name(rawValue: "subscriptionChanged")
}

class NotificationExample {
    var observer: NSObjectProtocol?

    func demo() {
        // block
        observer = NotificationCenter.default.addObserver(forName: .subscriptionChanged,
                                                          object: nil,
                                                          queue: .main) { notification in
                                                            print("Subscriptions changed")
        }

        // selector
        NotificationCenter.default.addObserver(self, selector: #selector(onSubscriptionChanged(_:)),
                                               name: .subscriptionChanged, object: nil)


        // sending?
        NotificationCenter.default.post(name: .subscriptionChanged, object: self, userInfo: [
            "subscribed" : [142]
        ])
    }

    @objc
    private func onSubscriptionChanged(_ notification: Notification) {
        dump(notification.userInfo)
    }
}

Let's create a more type-safe way of doing this, leveraging Swift's syntax and strong type safety.

Creating Protocol for TypedNotification

To have Swift-friendly notifications, we will create a protocol of TypedNotification with the name of the notification, the type of object sending the notification. Implementers who adopt this protocol can also carry with it any data they need.

We will also have an extension for our typed notification to provide a default value for notificationName. This way we only have to define a string when creating our own notifications.

protocol TypedNotification {
    associatedtype Sender
    static var name: String { get }
    var sender: Sender { get }
}

extension TypedNotification {
    static var notificationName: Notification.Name {
        return Notification.Name(rawValue: name)
    }
}

Creating Protocol for TypedNotificationCenter

We will create a protocol for TypedNotificationCenter that will be in charge of posting these typed notifications. The addObserver defines the type of notification, along with the sender type. We will use the block-based notification from NotificationCenter and return an instance of NSObjectProtocol to maintain the subscription.

protocol TypedNotificationCenter {

    func post<N : TypedNotification>(_ notification: N)

    func addObserver<N : TypedNotification>(_ forType: N.Type, sender: N.Sender?,
                                            queue: OperationQueue?, using block: @escaping (N) -> Void) -> NSObjectProtocol
}

Now we will extend the NotificationCenter to adapt and conform to the TypedNotificationCenter.

While posting, we will pass the name of the notification, object as sender and userInfo with value as the notification under a known key.

While observing, we will check for typedNotification in userInfo using the same key. Once we have our TypedNotification object, we will pass it on to the block.

extension NotificationCenter : TypedNotificationCenter {
    static var typedNotificationUserInfoKey = "_TypedNotification"

    func post<N>(_ notification: N) where N : TypedNotification {
        post(name: N.notificationName, object: notification.sender,
             userInfo: [
                NotificationCenter.typedNotificationUserInfoKey : notification
        ])
    }

    func addObserver<N>(_ forType: N.Type, sender: N.Sender?, queue: OperationQueue?, using block: @escaping (N) -> Void) -> NSObjectProtocol where N : TypedNotification {
        return addObserver(forName: N.notificationName, object: sender, queue: queue) { n in
            guard let typedNotification = n.userInfo?[NotificationCenter.typedNotificationUserInfoKey] as? N else {
                fatalError("Could not construct a typed notification: \(N.name) from notification: \(n)")
            }

            block(typedNotification)
        }
    }
}

Trying it Out

Here we create a sample notification with some strongly typed data and subscribe to it. Notice that the observer is passed the actual notification instance, making it easy to extract the data associated with the notification in a type-safe way.

struct SubscriptionsChanged : TypedNotification {
    var sender: Any
    static var name = "subscriptionsChanged"

    var subscribed: Set<Int> = []
    var unsusbscribed: Set<Int> = []
}

_ = NotificationCenter.default.addObserver(SubscriptionsChanged.self, sender: self, queue: nil) { notification in
     print("Subscribed to: ", notification.subscribed)
     print("Unsubscribed from: ", notification.unsusbscribed)
}

NotificationCenter.default.post(SubscriptionsChanged(sender: self, subscribed: [123], unsusbscribed: [567]))
blog comments powered by Disqus