Strong-typed Notifications in Swift
Making Notifications type-safe and reactive with RxSwift.
Notifications are a very useful mechanism of communication that lets us connect decoupled parts of our system through events. It is based in the GOF observer pattern. We post a notification when something occurs and many objects can react to it.
As useful as it might be, the off-the-shelf mechanism provided by Apple has many problems. Mainly because it is not type-safe. It relies on strings and parameters passed inside dictionaries, which introduces boilerplate code and can cause runtime errors.
In this article, I will explore how we can create a much better solution using protocols and extensions. So we could write notifications like this:
What’s wrong with vanilla Notifications?
NSNotificationCenter uses strings to identify notifications and parameters. We pass parameters inside dictionaries that can contain elements of any type. Like this:
To react to a notification, we must add an observer for it. The observer processes the notification and unwraps the parameters from the userInfo dictionary. It’s important to unwrap and cast the parameters properly:
Finally, we must not forget to remove the observer after we’re done listening. We typically need this to avoid memory leaks and unwanted side effects.
So, to use notifications we need to: 1️⃣ create the notification name, 2️⃣ put parameters into a dictionary and post the notification. Then 3️⃣, we must listen to the notification using an observer, 4️⃣, process it and unwrap the parameters. Finally, 5️⃣ we need to remove the observer.
Too much work, too many problems,
This approach is error-prone. We can make a typo and the notification stops working. We can define two notifications with the same name and we will have name clashes. Or we can forget to remove the observer and create a leak. Worst of all, these errors will usually appear at run-time.
There is also the risk of crashing the app when incorrectly unwrapping the parameters, with a type-cast error. The compiler doesn’t have the necessary information and cannot help us to prevent this.
In addition, there is a lot of boilerplate code. We must create the notification name, write a string to identify it, create a dictionary, remove the observer, etc. This tedious process occurs over and over again.
- Error-prone, string-based.
- Boilerplate Code.
- No help from the debugger, no autocompletion.
- Possible App Crashes and unwanted effects.
The out of the box solution is error-prone, it needs significant boilerplate code and it relies on strings and dictionaries. Because it is not type-safe!
When we write notifications, we actually know the type of parameters that are necessary. We could use that knowledge to let the compiler work for us. Also, we could avoid using strings as identifiers and use enums or structs.
So, we could have strong-typed notifications:
I wrote some extensions on top of
NSNotificationCenter to be able to write notifications like in the example above. My goal was to have a type-safe, reactive and easier-to-use mechanism. So, I wrote rxswift-notifications. Since I work with reactive programming a lot, it is based on RxSwift and RxCocoa. You could achieve something similar without using Rx.
Basics: How to use Reactive Notifications.
I will explain how to write and use notifications before I explain the implementation details.
1. Create the notification
First. We need to create a struct, enum, or class, that will represent the notification using the type system:
You might have noticed the
Notifiable protocol. This adds the boilerplate code necessary to post and listen to notifications. It also defines the notification names automatically and forces us to define the
ParameterType. The protocol will wrap the parameters into the userInfo dictionary automatically. I’ll explain this below.
2. Subscribe and react to it
Second. Subscribe to the notification and use the parameter directly. Notice that this is type safe, we don’t need to extract the parameter and cast it. The extensions will safely do that. Then we can use it like any other instance of
Finally. The disposeBag will remove the observer automatically when it disposes of the observable subscription. No need to remember to remove the observer.
I created some extras which I think are very helpful and I use a lot in my codebase.
1. Notifications with no parameters.
NoParamsNotifiable, lets you post notifications that carry no parameters.
2. Notifications with multiple types
Sometimes we group notifications together that carry different types of parameters. We could begin with a single type, like the example above with Persons, and need a new one. For example, a person might buy a vehicle:
GenNotification<T> Is a generic class that lets us put static variables inside enums to mix types in a category of notifications. For example, I could have ScreenNotifications that need to send different types of parameters on
This a class I use a lot when I need to listen to a bunch of notifications in a particular view controller or singleton. Instead of putting all the listening code in that particular object, I extract it and put it in a proxy that listens to all the notifications and notifies the subject.
This allows me to have objects that do not depend on the notifications and can be tested in isolation.
For example, if multiple instances of the same view controller are present, and I don’t want all of them to react to notifications, I create a listener proxy for only that instance.
Now that I’ve explained how to use the set of tools, it’s time to talk a little bit about how everything works.
The approach is based on a protocol that I called
Notifiable. A notifiable object, struct or enum, knows how to post a notification to
NSNotificationCenter. It also knows how to wrap and unwrap the parameters, safely, and strongly typed. Finally, it knows how to add observers and notify them when the notification occurs.
The extensions on that protocol do all the necessary work. We only need to adhere to the protocol and specify the type of parameter that the notification will contain.
Notifiable can be posted and has an associated
ParameterType. Since we want to listen to a notifiable, we need to create an observable from it.
The observable will produce elements of the correct type, each time the notification takes place. So, we can observe the stream and get the parameter right away.
In order to have this, we need a stream that produces a notification object each time the notification is posted. From this stream, we will extract the parameters to have a final observable. An observable that emits parameters of the correct type.
NotifiableObservable is the parent protocol that helps us to achieve this, binding things with Rx.
We need a way to identify notifications and to produce elements in a stream each time the notification is posted:
With the above code, we have notification names for free, and observable streams thanks to RxCocoa. Now we can have an observable that produces notifications.
All we need is to map the parameters from the notifications in the stream to have an observable that produces the correct-type elements.
Notifications can be much better, safer and more elegant if we use the type system. This set of protocols and extensions fits great with a reactive codebase. The effort to make notifications strong-typed is very little. The benefits are great.
Easier to use.
We only need to define the notification and parameter types, post it and react to it using flatMap or subscribe. Like we would do with any kind of observable.
- It leverages the power of the compiler. Doesn’t rely on strings.
- We don’t need to wrap, unwrap and cast parameters.
- We don’t need to remember to remove the observer.
- No run-time errors, no leaks.
- It avoids name clashes automatically.
Less boilerplate code.
- Fewer things to remember, no string related or observer removal boilerplate code.
Personally, it feels really good to have strong-typed notifications. Less code, safer apps.
What do you think? Do you have any comments or would do things differently? Is this something you would add to your projects?
Thanks for reading!