Working with Swifty NSNotification
Tip4 October 2020
NSNotification
s 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 NSNotification
s. 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 toorderIsReady()
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 invokeremoveObserver(_:)
orremoveObserver(_:name:object:)
before the system deallocates any object thataddObserver(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 extendingNSNotification.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.