Get your copy of the book

Transforming the Purchasing Experience

Download
Ebook Retail Transformation Technology

Still Using NSNotificationCenter? Try These Alternatives

  —  
 read

When developing a system in which one module triggers some change, which in some way affects the other, you may stumble upon the problem of communication between non-related code modules. It could be modifying some resource that is used by both modules. If a module does not listen to the changes, it will have the incorrect data/state, which means that views depending on that state will show the wrong UI to the user.

A common way of resolving the mentioned problem on the iOS platform is NotificationCenter. Nevertheless, some other alternatives can lift your code and increase type safety.

Let's explore some of them, along with examples.

Communication patterns on iOS

Communication between iOS objects can be implemented in various ways, the most popular one being the delegate pattern. It's a pattern where some object delegates the work that needs to be done to another object.

The most prominent examples would be UIKit objects (UIViews/ViewControllers), because most of the interaction we have with them is based on the delegate pattern.

Let's take a widely-used example: the UITableView. Both datasource (used to specify list content) and delegate (used for list UI customization) properties on table view are used to delegate work to an object that uses it. Most often, that will be the view controller.

With the evolution of the Swift closure syntax, we can use closure callbacks for that purpose. There are other older options like key-value observation and target-action pattern, but those are tied to the Objective C runtime, so we won’t get too deep into that.

Further along, the delegate pattern is often used for event callbacks between two screens in the navigation stack. Delegates are commonly passed via dependency injection, which can be seen in the following snippet:

// Code inside view controller
let nextVc =  SomeViewController()
nextVc.delegate = self
navigationController.pushViewController(nextVc, )

// For example, In VIPER architecture, inside Wireframe/Router
func showNextScreen(delegate: Delegate)  {
    let nextPresenter = NextScreenPresenter(, delegate: delegate)
    wireframe.show()
}

That’s nice and all, but what happens with screens/view controllers that are not correlated? How will they communicate events and data?

Think about the situation when you have some distant screens in your app and change on one of them will need to reflect on the other? In a tab-based application, you could have profile info hidden somewhere under one tab navigation stack. The second one, perhaps a settings screen in some other tab, wants to tell the first one: Hey, I changed the settings that make your current data incorrect, you have to reload!

It is a problem when you can’t inject the first screen as a delegate because you don’t have its reference by the time you are creating the second one. In this example, the settings screen is clueless about the profile info screen. Also, what if the settings screen wants to inform more than one screen about the change? In that case, the delegate pattern won’t help because, in its nature, it is used for 1-to-1 object communication.

How to solve that problem?

We must have some shared subject where interested screens/views can subscribe and observe for some app-wide events. The Subject, let's call it that, will hold the list of subscribed objects (Observers) and notify them accordingly when some change happens. How does the Subject know a change has happened? The place (view) where the change occurs has to inform the Subject on the change. The Subject should provide the interface for specifying that events have happened. Also, the Subject does not need to know about who its Observers are. They can be subscribed to a particular topic/event, as can be seen in Pub-Sub pattern, a loose-coupled implementation of the Observer pattern pattern. It will basically act as a middleman/event channel between publishers and subscribers/observers.

When the subject is informed of a change, it will react accordingly, i.e. it will notify all observers.

Exploring NSNotificationCenter and alternative solutions

Imagine that you’re building a music app and working on a screen that shows a list of songs. This song list changes over time, for example, when a user adds a new favorite song. Whenever this list changes somewhere in the app, the song list screen has to refresh.

Let’s say we want to show a SongListViewController which will show the list of songs in a UITableView. Also, it will handle the data source change via the didUpdate(song:, action:) method where the song property is a song model, and the action is an enum that represents three possible cases of actions available for a song: Update, delete, and create.

func didUpdate(song: Song, action: Action) {
    switch action {
    case .update:
        guard let index = datasource.firstIndex(where: { $0.id == song.id }) else { return }
        datasource[index] = song
    case .delete:
        guard let index = datasource.firstIndex(where: { $0.id == song.id }) else { return }
        datasource.remove(at: index)
    case .create:
        datasource.append(song)
    }
    tableView.reloadData()
}

The common solution: NotificationCenter

Apple has provided a Pub-Sub pattern for change observation in the Cocoa library called the NSNotificationCenter (NotificationCenter in Swift). NotificationCenter is a place where all notifications are posted to and are observed from. Each notification must have a unique way to identify themselves, which is the NSNotificationName (Notification.Name in Swift). Similarly, if we were to observe, or listen, to any channel, we would call on the observe method available to us through NotificationCenter.default and perform some type of action based on this listening. There are two ways we can subscribe to changes in the NotificationCenter:

  • selector-based (target-action pattern)
  • closure-based

Selector based

// Note: No need to manually remove observer on deinit here, since we are using selector based addObserver
// Subscription is removed automatically since iOS 9
public class SongListViewControllerNCSelector: SongListViewController {

    private let notificationCenter = NotificationCenter.default

    override public func viewDidLoad() {
        notificationCenter.addObserver(
            self,
            selector: #selector(receiveSongDidUpdate(notification:)),
            name: notificationName,
            object: nil
        )
    }

    @objc func receiveSongDidUpdate(notification: Notification) {
        guard
            let updatedSong = notification.userInfo?["song"] as? Song,
            let action = notification.userInfo?["action"] as? Action
        else { return }
        didUpdate(song: updatedSong, action: action)
    }
}

Closure/block based:

public class SongListViewControllerNCClosure: SongListViewController {

    private var observationToken: Any?
    private let notificationCenter = NotificationCenter.default

    override public func viewDidLoad() {
        super.viewDidLoad()
        observationToken = notificationCenter.addObserver(forName: notificationName, object: nil, queue: nil) { [unowned self] notification in
            guard
                let updatedSong = notification.userInfo?["song"] as? Song,
                let action = notification.userInfo?["action"] as? Action
            else { return }
            self.didUpdate(song: updatedSong, action: action)
        }
    }

    // On deiinit of class that wraps/holds the observation token you should remove subscription
    // Otherwise it will stay alive, this only happens when using closure based addObserver method
    // As discussed here https://oleb.net/blog/2018/01/notificationcenter-removeobserver/

    deinit {
        notificationCenter.removeObserver(observationToken)
    }
}

Pros

  • using a natively provided solution from Foundation

Cons

  • it is not type-safe – you have to wrap the object of the change under the userInfo parameter, which is a [AnyHashable: Any]? dictionary – downcasting the Any placeholder type on the receiver side
  • relying on a dictionary key which most of the times will be some string – that became better with Notification.Name
  • closure based subscription – you have to unsubscribe manually, or else you’re bound to have memory leaks popping up

Note: In a multithreaded application, notifications are always delivered on the thread in which the notification was posted, which may not be the same thread in which an observer registered itself. That may be important when updating UI after a model change in the background that posts notification (you would have to dispatch to the main queue).

Custom solution

If you are an experimental person and tend to implement custom solutions, you could go and see what can be improved in NotificationCenter. For example, making a subscription code in Swift’s type-safety spirit. The implementation of Subject that holds the subscriber can be seen here.

We can expose methods that are required for a particular observation use case in a separate protocol, in this case called SongObserver. Also, to distinguish observation use cases, we can use an enum which will define what can be observed in our app, ObservedType.

protocol SongObserver: Observer {
    func didUpdate(song: Song, action: Action)
}

enum ObservedType {
    case song
}

Also, our Subject has to expose some interface which will be used for subscription, defined in a protocol.

public protocol SubjectProtocol: class {
    func subscribe(_ observer: Observer, for types: ObservedType)
       //…rest of protocol methods
}

Our view controller can now use the Subject for subscribing to song changes:

class SongListViewControllerCO: SongListViewController {
    private let subject: SubjectProtocol = Subject.shared

    deinit {
        print("SongListViewController: released from VC stack")
        subject.unsubscribeReleasedObservers()
    }

    override func viewDidLoad() {
        print(SongListViewController: subscribing for song changes)
        subject.subscribe(self, for: .song)
    }
}

Since SongListViewControllerCO inherits from the base controller (SongListViewController) which already implements the didUpdate(song:, action:) method, we can just conform to the SongObserver interface and we are good to go.

extension SongListViewControllerCO: SongObserver {}

This approach works well because it offers us type safety. However, the downside is (as with any custom implementation) that you have to do most of the heavy lifting to get everything started: subscriber list filtering, managing subscriptions, observer lifecycles, thinking about memory leaks, and so on.

Rx to the rescue

RxSwift is part of Rx/ReactiveX family, which is a cross-platform API used for asynchronous programming using observable streams. Everything that can produce values in some period is an Observable or a stream of events/values. Those streams can be composed and transformed via a massive collection of operators that Rx gives us on the plate. Subscribe to stream, and voila!, you are ready to go.

A good example of a stream can be a chain of API calls that go one after another or messages received through a socket. In our example with songs, we can represent updates on a song as a stream of actions that happen on a specific song.

Let's see how our song observing example would look like using RxSwift:

public class Subject {
    public static let shared = Subject()
    private init() { }
    public var songDidUpdate = PublishRelay<(Song, Action)>()
}

public class SongListViewControllerRxBasic: SongListViewController {
    let subject = Subject.shared

    // Subscriptions are disposed by the DisposeBag when VC deallocates
    private let disposeBag = DisposeBag()

    override public func viewDidLoad() {
        super.viewDidLoad()

        subject
            .songDidUpdate
            .asObservable()
            .subscribe(onNext: { [unowned self] (song, action) in
                self.didUpdate(song: song, action: action)
            })
            .disposed(by: disposeBag)
    }
}

In the example above, you can see that subscribing to changes of an object can be easily made with RxSwift. When an object is updated, onNext closure will be called. When the song is updated with some action, Subject.shared.songDidUpdate.accept((song, action)) can be called from any place to notify subscribers. In some cleaner architecture, this Subject would be commonly passed via dependency injection, and it would be hidden behind the interface so it could be mockable for unit tests, but that’s a subject for another discussion.

Side note: PublishRelay is a type of observable that can also push/accept values into the stream. Think of it as a pipe that can emit elements, but can also accept new ones from outside.

Also, we can go even further and use RxCocoa that provides a beautiful set of reactive extensions for Cocoa/UIKit views. Bindings are nicely written with the declarative style, which is a great thing for readability. For example, if UITableView is one’s weapon of choice for showing lists, RxCocoa offers convenience for binding the data source stream to its cells. This means reloading the cells whenever the data source changes are given to us out of the box. There is no need to implement UITableViewDataSource methods and reloading data manually on tableView when the source of data changes. Works like a charm!

// UITableViewDatasource implementation not needed
public class SongListViewControllerRxBinding: SongListViewController {
    private let songStore = SongStore.shared
    private let disposeBag = DisposeBag()

    override public func viewDidLoad() {
        super.viewDidLoad()

        // Datasource is defined in code below by binding to tableView.rx.items (reactive way), clear the delegate just in case
        tableView.dataSource = nil

        songStore
            .songs
            .bind(to: tableView.rx.items) { [unowned self] (_, _, item) in
                // Closure for returning cell that is configured with song item
                let cell = self.instantiateCell()
                cell.configure(with: item)
                return cell
            }
            .disposed(by: disposeBag)
    }
}

Combine & SwiftUI

As many readers already know, Apple has provided SwiftUI, a new UI framework that allows the declarative definition of views and binding them to the data in a reactive/async manner using the Combine framework. We have already written a lot about it, feel free to check our initial post on SwiftUI. Also, make sure you are in the loop with the SwiftUI 2.0 changes

Combine is pretty similar to RxSwift and if you already mastered or played around with RxSwift code it should be no problem to at least scratch the surface of Combine. There is also a cheatsheet for an easier move from RxSwift to Combine. Combine is a key player for delivering awesome new features that SwiftUI provides such as two-way binding, automatic refreshing of the views that depend on explicitly defined state variables. Also, the usage of property wrappers as a relatively new Swift feature makes it all less boilerplate. Let’s take an example with the list of songs and now implement it SwiftUI!

ObservableObject

In the example below, SongStore is created and will be used for both storing the songs and notifying the changes. SongStore conforms to the ObservableObject protocol, which means it can send a change to its subscribers just before any of its property changes by using default objectWillChange Publisher (specified by the protocol itself).

Publisher can be thought of as Observable in RxSwift world, something that emits a stream of values. When ObservableObject is used in combination with the @Published property wrapper, Swift compiler provides synthesized protocol implementation so it emits changes to the Subscriber when any of its @Published properties change. It is anti-boilerplate shorthand so the objectWillChange.send() method does not have to be invoked manually by the programmer when some property changes.

The example below shows how the SwiftUI view will connect to a SongStore to show song and automatically refresh on change in a song list.

public class SongStore: ObservableObject {

    private init() {}
    public static let shared = SongStore()

    // When value is changed, publisher under the hood will automatically trigger send()
    // and view that observes this will automatically update its body
    @Published public var songs: [Song] = HardcodedSongs.ironMaiden
}

extension SongStore {
    public func didUpdate(songs: [Song]) {
        self.songs = songs
    }
}

This is what our view will look like:

extension Song: Identifiable {}

struct SongListRow: View {
    let item: Song

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(item.name)
                .font(.headline)
            Text(item.author)
                .font(.subheadline)
        }
    }
}

struct SongList: View {

    // Whenever this object changes (when song array changes) view will re-render it’s body
    @EnvironmentObject private var songStore: SongStore

    var body: some View {
        List {
            ForEach(songStore.songs) { song in
                SongListRow(item: song)
            }
        }
    }
}

If you are seeing SwiftUI code for the first time you will maybe have mixed feelings. It looks easy to read because of its declarative nature, but how does it really work under the hood?

Under the hood

In the code above we can see the implementation of a plain SwiftUI view for representing one row in the song list (SongListRow). It will just show Song in a vertical stack in a title-subtitle manner. SongList will be used as a view that will show list of songs where each song is represented with SongListRow. Here we are using SongStore as an environment object which means it is appropriate to be used by other views in the environment. Since SongStore is ObservableObject, whenever it changes, the list will automatically re-render.

This happens automagically because @EnvironmentObject property wrapper indicates that the songStore property is also a state of SwiftUI view. Whenever state changes, SwiftUI will automatically refresh the part of a view that depends on it. What happens in the background is that SwiftUI view subscribes to objectWillChange Publisher that ObservableObject uses to emit changes.

The advantage of using EnvironmentObject is that now our shared instance of SongStore is in the environment and ready to be consumed by other SwiftUI views just by specifying @EnvironmentObject inside the property definition. By that, the injection has to be done only once (via environmentObject modifier). Another benefit is that all dependent views refresh when some of them modify the environment object. Since all views point to the same model in the environment, if one view changes that model, all views will immediately re-render.

let songList = SongList().environmentObject(SongStore.shared)

More about the different state variables and what purpose they can be used can be seen in last year’s data flow WWDC session. Fresh-out-of the-oven WWDC updates from this year can be seen in this video. There is also a nice cheatsheet shared by Chris Eidhof (founder of objc.io) when to use which property wrapper if you ever get in a dilemma. Also, a nice page expands on it in a couple more words.

Several alternatives to NSNotificationCenter

There are many approaches to how your code can react to app-wide changes, so developers have many options to choose from. As seen, there isn't just the NotificationCenter, but also RxSwift and Combine on the plate. Of course, if you like to experiment, you can always come up with your own system for app-wide callbacks.

If you’re interested in them, playgrounds project of examples mentioned in the article can be found here, and if you have any tips & tricks of your own, do share!

The mood Mario Kovačević painted in the cover illustration can only be described as electric.