Implementing a Custom NotificationCenter Efficiently in iOS
Various ways to use the Observer class
If you work with object-oriented programming, certainly you are familiar with (or at least have already heard about) the Observer
design pattern.
This pattern consists of an Observable
class that emits events
with custom data
attached to it. Other classes that subscribe to it may get a notification when a specific event is triggered.
This pattern is important to the iOS environment and is used across APIs, such as NSNotificationCenter
,Combine
and RxSwift
. The observable
class emits events that may be triggered internally or externally. To listen to the changes, the Observer
classes should register to receive notifications.
OK, But We Already Have Delegate and Closures for That
I understand that relying on a complex pattern like Observer
may be “killing a bee with a cannon.” For example, you may send an update from a ViewModel
to the ViewController
via NotificationCenter
. But the core advantage of this is creating a class that broadcasts an event to any other class interested in that event without knowing who may be notified.
Let's say we have a ViewController
with a UIButton
. When this button is clicked, we want several places of the application to be notified and update the UI by showing a banner or something else. You could also notify multiple classes about a network event too. It doesn't matter how it’s handled, but it’s just like Netflix — we broadcast notifications about a new series without knowing who will/how many will receive them.
That's the idea of Observers
. A few or millions may need to be notified about a single class’ event.
But what is the magic behind that? How do we notify many observer
s without a delay? That's what this article is for. We will implement our own custom NotificationCenter
to save a collection of different yet related events and observers. This will make it so that any time our NotificationCenter
receives an event with some attached data, we can broadcast that to multiple places. Let's get started.
Protocols: Observable and Observer
Two core entities in the project communicate in the same pattern. There is an Observable
entity that emits events with generic attached data and allows multiple Observers
to listen to events from there. Three possible actions can be delegated to the Observable
:
notify
: tells theNotificationCenter
to broadcast messages with related dataregister
: tells theNotificationCenter
to register a newObserver
to receive notifications regarding some eventunregister
: tells theNotificationCenter
to unregister someObserver
instance, so it won’t receive any other notifications regarding an event
Creating the NotificationCenter
Now that we have our protocols for establishing the relation Observable-Observer
, we should implement our core class for the CustomNotificationCenter
:
We implemented the NotificationCenter
as a Singleton since we want a central and unique network to broadcast messages across all parts of our application. This is a classic use case for the Singleton design pattern because we don't desire multiple instances for NotificationCenter
. So, we have a declared shared
instance that can only be instantiated once due to the private init
. We also declared all three functions in our protocol.
Saving Observers and Events
Now that we have our NotificationCenter
class, we are ready to implement our data structure to save the possible events and observers that subscribe to each event. Before declaring the observer
s, let’s think about the best way to save this kind of data so it can be broadcast to all observer
s efficiently. The most standard way is to create a data structure to save an Observer
and an array of strings with all the events the instance observes. Here’s what that looks like:
OK, this solution works, but it's not an efficient way of saving the observer
s. When receiving an event to broadcast, we should iterate through the entire observers
array and check for each element if the event is registered in the data structure. In any case, we would need to iterate through the entire array (O(n))
. It would take a lot of time if we had hundreds of items.
The best way to solve this complexity issue is to provide a way of accessing only the observer
s registered in that given event. Are there any data structures whose access operation takes O(1)
time? Definitely a HashMap
, or better yet, a dictionary! Now, let's reshape our observer
s and save a list of them, so we know all the objects subscribed to an event when triggered. Here’s the code:
Now we can get straight to the point and iterate only through the observers that subscribe to the given event.
Implementing Observable operations
Now that we have a data structure to save events and registered observers, let's implement our three core operations within our NotificationCenter
:
As you can see, when we’re registering a new observer
to a given event, we should check if it's not already registered to avoid duplicates. After that, we should check if the array is nil as we are dealing with a dictionary. This will save that in a key, and if it is, we instantiate the new array with the single Observer
element. Otherwise, we append it to the existing array.
When we want to unregister an observer
, we should only remove elements in the array for the event key that references the same memory address as the observer
object. This is why we declared the concrete types for the Observer
protocol as reference types.
If we want to notify all observer
s about an event, we should iterate through all observer
s in the array for the event key. After that’s done, we should send the attached data to call the receive
method and notify it. This method is of type Any
, so we can broadcast anything. The Observers
have the task of casting the data instance with the event and checking if it's the expected one.
Testing our NotificationCenter
Now that we have implemented our NotificationCenter
mechanism following the Observer
design pattern, let's create three different classes: one for sending an event to the NotificationCenter
, and two others to implement the Observer
protocol and handle events. Here’s the code:
As you can see, we declared a class Sender
that manually sends events with custom data to the NotificationCenter
. We created two other classes, Observer1
and Observer2
, and they registered themselves as observer
s to the NotificationCenter
. These classes registered by creation time and implemented a receive
method from the Observer
protocol, expecting to receive an Event
with an integer as the data. The Sender
instance should trigger the event, and the observer
s should respond to it. Let's test what we have:
OK, here’s what we have:
As expected, that's brilliant. We receive outputs from each Observer
after sending an event with the expected data format.
Conclusion
In this article, we spoke about the Observer
design pattern that exists across most object-oriented programming languages. Also, we implemented a NotificationCenter
that works in the same way as Apple's native one.
We presented the pros and cons of broadcasting messages with a NotificationCenter
over the old Delegate
and Closure
mechanisms and discussed an efficient way to save events relating to observer
s in our NotificationCenter
class.
Finally, we learned how to register, unregister, and notify them without needing to search all the items to find the registered ones.
This knowledge is required for many whiteboard challenges in US companies. I hope you are prepared for that and, of course, enjoyed the content ;).