Redesigning Apple’s NotificationCenter

Benedek Kozma
Tresorit Engineering
7 min readJun 25, 2019

NotificationCenter is one of the patterns used for communicating between objects on Apple platforms. It has been part of the public API of Foundation since iOS 2.0 and macOS 10.0, so I’m sure most of you who are developing for Apple devices know how it works. Its Swift API is just a direct translation of the old Objective-C API, so I decided to rethink it. (If you are only interested in the final code, you can find it on GitHub)

There are multiple ways to communicate between objects in an application and each method has its use cases. Before we start off with our project, let me give you a rundown of these commonly used patterns in the Apple ecosystem (skip to the redesign):

Delegation is probably the most well-known pattern in protocol oriented programming, it is used to decouple your class that can be used in multiple places across your project and provide a one-to-one channel for communicating between an object of this class and its delegate. Table and Collection Views are probably the best example for this, their delegate (and data source) protocol is so verbose, you probably even want to use a separate object just to handle them and then translate it to your own delegate that your ViewController will implement, for example when adding reordering to a CollectionView, your ViewController should only handle one delegate function instead of three:

Responder chain, which Apple uses mostly for finding the object to handle touch/mouse/keyboard events, is similar in a way that it also provides a one-to-one channel, but your different “delegate” methods might be handled by different objects as the first one that implements them in the chain will handle it. One interesting use case I know about is for a possible implementation of the Coordinator pattern, you can find the code for it by Aleksandar Vacić on GitHub.

https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events

Key-Value observation (KVO) provides a one way one-to-many communication channel. You need to subclass NSObject to use it (or implement your own) and then your stored properties will be automatically observable (and if you define dependencies for calculated properties, they can be observed too). The use case I have seen the most often is for resizing your popover when the content size of the TableView inside changes.

And then finally the topic of our post, NotificationCenter. It is very similar to KVO, but it can also be used when you don’t know the object you are observing. For example, in our file sharing app at Tresorit, we have the option to upload files. These uploads (and also downloads) have to be managed by a single object to avoid making too many connections at once. This object is not tied to a specific screen, and multiple views might have to be updated when something changes. Our file browser has to add a new row to the table (if it’s in the same path as the upload is), that row has to display the progress of the upload, and we also want to have a separate view where you can see only the currently running transfers in a similar fashion.

Why do I want to redesign Apple’s solution?

It was originally designed with Objective-C’s behavior and limitations in mind. Since Objective-C doesn’t use generics and associated types to easily (type)check the inputs, the inputs themselves have to be generic and then the user has to do the necessary checking instead when writing their application.

It also has the problem of the notification name being string based, which can cause a problem if the previous developer working on the project didn’t use constants and just typed out the name every time, potentially making a typo in a feature that wasn’t tested properly. Another possible source of unexpected behavior is when an external dependency decides to use the same notification name internally that we want to use.

My process of the redesign

I didn’t want to fully redesign the API for version 1.0 but rather just use a copy of the current API with associating a sender and a payload type to the notification.

Since the core mechanism of NotificationCenter will always be the same, it will be really easy to change the API around a working and tested framework once I decide what direction would be the most convenient to go in.

When making a framework like this, the first step should always be defining the flow, making a minimal implementation and writing some tests for it. Since this is a performance critical framework (when sending a lot of notifications in a short time, we don’t want our framework to be the bottleneck), we will probably need to improve the performance once we have a way to measure it. Writing tests is really easy, you just write down different use cases about how you would want to use the framework and what would you expect to happen and then once you are done, you should have 100% code coverage (My choice for CI and code coverage was Travis CI and Codecov as they are free for open-source projects and I had previous experience with them). Of course, even if code coverage says 100%, that doesn’t necessarily mean that all the possibilities are covered with tests, so don’t be afraid to add more tests later on, even if they don’t increase the displayed coverage percentage.

As my NotificationCenter could be used from multiple threads, it’s essential to keep the code thread safe, which means tunneling the access of our observer storage through a queue. Posting has to synchronise with our queue but it can be done concurrently, while adding or removing an observer can be done asynchronously but we need to guarantee that nobody else is accessing the storage at the same time.

After we know that our code is working, we should add some performance tests to be able to measure the impact of changes and to compare it with Apple’s solution. This requires us to create a bunch of dummy notifications and I think it’s the best to write a script to generate the code for it since Swift doesn’t allow having an array of protocol implementations where the protocol has associated types and we need to repeat the same task (observing or posting) for all of them.

Since I went with a similar approach to the swift-corelibs-foundation implementation first (which is a reimplementation of Apple’s Foundation for non-Apple platforms), I didn’t expect it to be too good and as expected, it had horrible performance.

After switching the implementation to one that uses multiple steps of dictionaries (If you want to have your keys based on memory address, you either use NSMapTable if you want to directly access your storage from Objective-C too, or you use ObjectIdentifier as key for your dictionary, which just bitcasts your pointer to Builtin.RawPointer and uses that value for hashing and equality checking and it also works for types), my version finally became comparable in speed to Apple’s.

“Wall clock” time relative to Apple’s NotificationCenter. Number of operations is chosen separately in each test case, so the reference test executes between 0,1s and 1s. Tests were ran on a baseline 2018 15" MacBook Pro.

As ObjectIdentifier doesn’t keep a reference to the object, I also needed to have a weak reference to the observed object to check if it’s the same one that we observed, or a different object got its memory address after it deallocated. At first, I was using compactMap to filter these out but since that means creating a whole new array for the observations to call every time a notification was posted, it was also a considerable slowdown, so I decided to switch to just returning Dictionary.Values instead and filter out the unneeded observations just before calling the block for them, which pushed my framework to be faster than Apple’s NotificationCenter even with optimizations turned off.

Since Apple’s frameworks already use a lot of notifications that might be used in a project, I also wanted to provide a way to have a wrapper for them that takes care of the unboxing (and boxing if you want to post) of those notifications. As the compiler needs to know the exact type when calling a generic function, we can just use the same method signature for the new observe and post methods but with our new BridgedNotification protocol and it is guaranteed that they won’t use the TypedNotificationCenter way of observing/posting.

As you can see, these notifications will use NotificationCenter.default instead of our own and just provide a wrapper for observing, posting and invalidating when our observation deallocates. As translating all the notifications along with writing tests for them would require a lot of work that might never be used and might also require decisions for values that have a minimum version higher than ours, I decided to just translate 1 set of them for v1.0.0.

My plans for v2

Currently, my only goal for v2 is to have a more intuitive API. For example if you have a UIButton.PressedNotification, you might want to write button.onPressed { … } and UIButton.onPressed { … } instead of TypedNotificationCenter.default.observe(UIButton.PressedNotification.self, object: button) { … }

The most obvious answer for this would be generating the code in the pre-build phase but that requires initiating a build before being able to use code completion, so if I can find a way to solve it with some generic/extension magic, that would be the best.

Final thoughts

In my opinion, writing a framework like this is really fun. It’s also a great practice for writing high performant thread safe code. Even though in most real-world use cases there is no difference in your code executing in 0.01 or 0.1 seconds, you still don’t want it to take 30 seconds.

As for the people, who are curious when they should have multiple notification centers, here is a quote from Apple’s docs:

Each running app has a default notification center, and you can create new notification centers to organize communications in particular contexts.

Those contexts are usually frameworks that have a noticeable amount of internal communication.

--

--