WOODY'S
FINDINGS

Working with Swifty NSNotification

Tip

4 October 2020

NSNotifications are a simple way to send messages or content throughout an application or a project. Although some better ways can be used to reach the same goal, as explained in this article from John Sundell, sometimes you have to work with NSNotifications. For example, some framework could send a notification to let any other object know that a specific event happened.

So in this article, we will try to use NSNotification in a more "Swifty" way. We will work with two models: Cook which sends an 'orderIsReady' notification, and Client which observes this notification.

Send and receive notifications

Let's first take a look at how notifications can be sent and observed with the APIs.
The most common way to send a notification is to use the function NotificationCenter.post(name:object:). This allows to send identifiable notifications with an optional object, like data or a structure - really anything as the object is of type Any.
Thus, the Cook structure could send an "orderIsReady" notification like so.

struct Cook {
    var center = NotificationCenter.default

    func notifyOrderReady() {
        center.post(name: NSNotification.Name("orderIsReady"),
                    object: nil)
    }
}

It is possible to directly use NSNotificationCenter to post a notification. But it is a good practice to allow another object to inject dependencies when testing.
If you raised your eyebrow when you saw that NSNotification.Name was instantiated with a local String, don't panic! We will come to it in the second part.

To receive notification, there are two possible ways. The first one will execute a function inside the object when the notification is received: addObserver(_:selector:name:object:). The second will use a provided closure: addObserver(forName:object:queue:using:).

For example, Client could register an observer in a startObserving() function.

final class Client {
    var center = NotificationCenter.default

    func startObserving(() {
        center.addObserver(self, selector: #selector(orderIsReady),
                           name: .init("orderIsReady"), object: nil)
                           
    }

    @objc func orderIsReady() {
        print("😋")
    }
}

  • The object parameter acts as a filter as it allows to specify an object from which the notification should be sent and thus to ignore the others.
  • The usage of a class is required since we add the @objc attribute to orderIsReady()

On the other hand, we could rather use a closure to execute orderIsReady(). In this case, no class is required.

struct Client {
    var center = NotificationCenter.default

    func startObserving(() {
        center.addObserver(forName: .init("orderIsReady"),
                           object: nil, queue: nil) { _ in
           orderIsReady()
        }
    }

    func orderIsReady() {
        print("😋")
    }
}

We can omit self in the closure to call orderIsReady with Swift 5.3 as Client is a struct in the last example. But for previous versions we would have written self.orderIsReady()

In this article, we will see how to improve registering of an observer with a closure. I find it "Swiftier" as it does not force the usage of target and @objc.
Also it is more flexible as we can write instructions directly inside the closure, which is useful if a notification delivery should not trigger many code executions.

Removing observers

One important point: when adding an observer, it is required to remove it from the center using removeObserver(_:) or removeObserver(_:name:object:). As specified in the documentation, when targeting iOS 9.0 and later or macOS 10.11 and later and using addObserver(_:selector:name:object:) (execute a function), removing the observer is not required as the system will take care of that for us.
However, this is still required when executing a closure. There are two possible ways for Client to handle that. The first one is to implement a removeObservers() function which should be called before the object is deallocated, as mentioned in the documentation:

You must invoke removeObserver(_:) or removeObserver(_:name:object:) before the system deallocates any object that addObserver(forName:object:queue:using:) specifies.

If we keep Client as a struct, the object that owns it should always know that it has to call removeObservers() on the Client instance before deallocating. This is not terrible, as it is highly error-prone.

The other solution is to change Client back to a class and use its deinit function - which is called when the class instance is deallocated. We will have to add a few things in order to make that work.
First, store the NSObjectProtocol observer returned by addObserver(forName:object:queue:using:) as a property because we will need it to remove it from the notification center.
Second, now that Client is a class again, we have to add self when calling orderIsReady() in the notification closure. Doing so, we will have to make sure that self is not deallocated when the notification arrives by capturing a weak reference of it (as mentioned in the doc again). Finally, we will add the deinit function.

It looks like so:

final class Client {
    var center = NotificationCenter.default
    var observer: NSObjectProtocol?

    init() {}

    deinit {
        guard let observer = observer else { return }
        center.removeObserver(observer, name: .init("orderIsReady"),
                              object: nil)
                              
    }

    func startObserving(() {
        observer = center.addObserver(forName: .init("orderIsReady"),
                                      object: nil, queue: nil) {
            [weak self] _ in self?.orderIsReady()
        }
    }

    func orderIsReady() {
        print("😋")
    }
}

I don't know you but all this code to observe a notfication feels a bit... heavy, doesn't it? Then let's try to improve all of that in a "Swiftier" way!

Extending notifications

Let's start by extending NSNotification.Name and NotificationCenter.

NSNotification.Name

Previously, we used a NSNotificationName with a raw value initilisation to specify the notification to post or to observe. If we could have defined a String constant somewhere, there is another common way. We will extend NSNotficiation.Name to define our notification(s) as static constants.

extension NSNotification.Name {

    static let orderIsReady = Self(rawValue: "orderIsReady")
}
This way, each time a notification has to be specified by its name, we will use its static extension constant. For example in the Cook structure, we could replace
func notifyOrderReady() {
    center.post(name: NSNotification.Name("orderIsReady"), object: nil)
}
with
func notifyOrderReady() {
    center.post(name: .orderIsReady, object: nil)
}
This is more readable and less error-prone, so there is no reason not to use it 👍

NotificationCenter

As passing a nil value for the object parameter in the several observer management functions is a bit painful, let's add functions that do that for us.

extension NotificationCenter {

    func post(_ notificationName: NSNotification.Name) {
        post(name: notificationName, object: nil)
    }

    func addObserver(for notificationName: NSNotification.Name,
                     using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
        addObserver(forName: notificationName, object: nil, queue: nil, using: block)
    }

    func removeObserver(_ observer: NSObjectProtocol, name: NSNotification.Name) {
        removeObserver(observer, name: name, object: nil)
    }
}
With the above in place, it is still possible to pass a non-nil object parameter when using the functions thanks to function overloading.

For the addObserver function, the queue parameter is set to nil in this article. Feel free to use another overload of addObserver() in your own code if you prefer.

Now let's stake a look at the new possible implentations of Cook and Client, shall we?

struct Cook {
    var center = NotificationCenter.default

    func notifyOrderReady() {
        center.post(.orderIsReady)
    }
}
final class Client {
    var center = NotificationCenter.default
    var observer: NSObjectProtocol?

    init() {}

    deinit {
        guard let observer = observer else { return }
        center.removeObserver(observer, name: .orderIsReady)
    }

    func startObserving(() {
        observer = center.addObserver(for: .orderIsReady) {
            [weak self] _ in self?.orderIsReady()
        }
    }

    func orderIsReady() {
        print("😋")
    }
}
Now that's clearer!
We could stop here and call it a day. But I am not totally satisfied with the class implementation of Client, especially the deinit part, and I would like to show you how to get rid of it. Interested? Read on! 👇

Introducing NotificationObserver

Basically, we will build a wrapper to handle the logic of an observer. This NotificationObserver wrapper will have a NSObjectProtocol property as well as a name to keep the observed notification name. This wrapper will implement the observer removal in its deinit function. Here it is.

final class NotificationObserver {

    var name: Notification.Name
    var observer: NSObjectProtocol
    var center = NotificationCenter.default

    init(for name: Notification.Name, block: @escaping (Notification) -> Void) {
        self.name = name
        observer = center.addObserver(for: name, using: block)
    }

    deinit {
        center.removeObserver(observer, name: name)
    }
}

Now, let's take a look at how Client can be refactored. Unfortunately, we can't get rid of the class for a struct. Escaping closures do not play well with a mutating struct and we have to mutate the struct to store the observer. But the good news is that we can get rid of the deinit part because removing the observer is handled by NotificationObserver. Also, the center property is not required anymore.

final class Client {
    var observer: NotificationObserver?

    init() {}

    func startObserving(() {
        observer = NotificationObserver(for: .orderIsReady) {
            [weak self] _ in self?.orderIsReady()
        }
    }

    func orderIsReady() {
        print("😋")
    }
}
If you want to make sure of that the observer is removed, you can download the playground file for this article with all the final implementations. At the end, a test is performed to make sure the observer is removed without any additional code from Client.

A few remarks

Handle several observers

Most often the classes or structs I implement have to observe more than one notification. In this case, it is a good idea to store observers in an array, and to extend Array<NotificationObserver> to easily add new observers.

extension Array where Element == NotificationObserver {

    mutating func append(name: NSNotification.Name,
                         using block: @escaping (Notification) -> Void) {
        append(NotificationObserver(for: name, block: block))
    }
}
So given an observers array var observers = [NotificationObserver](), we would write
observers.append(name: .orderIsReady) { [weak self] _ in self?.orderIsReady() }

Using a struct

I would prefer to use a struct rather than a class to implement Client, but I could not find a way to make the compiler silent. If aynone reading this was able to use a struct that stores an observer, please contact me 🙏

📡

I hope you found this tip useful! If so, feel free to share it on Twitter. If you want to reach out to me or to know when new posts are available, you can find me on Twitter. Also you can send me an email.