A Swifty NotificationCenter


During the last few months I’ve been quite busy together with the rest of the team over at WeltN24 building the revamped Edition App. We’re now at 4.5 stars on the AppStore and we consistently get about a thousand user reviews on every update we publish. The management is happy with the outcome and we achieved a lot, especially on the technical side.

The challenges were big: a team of 3-5 iOS developers, who never worked with Swift, was tasked with rewriting a successful and long-living (5 years) application in the shiny new Swift (1.0!). We started slow and explored the language, and we found ourselves discovering the same patterns that everybody is reading and raging about nowadays on the internet.

The time to write something about each and everyone of these was definitely not enough, so I ended up reading with some envy and disappointment over myself for not writing about these things before, but I thought to myself that if I started with something small, I could write something at least. So here I am today, talking about a Swifty approach to the almighty NSNotificationCenter.

The problem

You may ask yourself “What’s the problem with NSNotificationCenter, a class we’ve been using for years if not even a decade?”. Well, I never liked the notification center too much, to be honest.

When I started working with Swift and fully embraced type-safety (keep in mind I was designing and thinking about something I called RuntimeNetworkFoundation (Github link) just over a year earlier!), I found myself complaining about a couple of things, for example:

The notification names are strings

I don’t like so much when I have to use strings in places where they don’t feel natural. I can understand when I have to do use them in storyboards (e.g. view controllers or segues identifiers), since after all they’re just a couple of XML files and as such they can only handle strings. But I also rejoy when I see projects that try to solve this problem, even though they require some pre-processing of the source files and code generation too, like SwiftGen for assets catalogs, localizable strings and storyboards. Using enumerations to reference segues, assets, and localizable strings looks just the way it’s supposed to be, and I think the notification center should also work this way. Also, I find a name like UIApplicationDidChangeStatusBarOrientationNotification unnecessarily long and tedious to write or read. But that’s another topic.

Subscribers are retained by the notification center

This is kind of outdated already, since with iOS 9 and OSX 10.11 NSNotificationCenter uses weak references for the subscribers, but when we started the project with iOS 8 this wasn’t the case and anyway subscribing through callback blocks still has the same issue, and I assume many people do since it’s much more convenient. Not that it’s too big of a deal, but I suspect some people are suffering from memory leaks and retain cycles because of this. It would be nice if one could subscribe and forget (just as this is the case now since iOS 9).

It doesn’t blend nicely in the app domain

Another feeling I have when dealing with NSNotificationCenter is that you don’t have much room to play. At the end of the day, you can of course post your own notifications, but you have to stick with a string for a name (that will have to be either a global constant or somehow associated to the class that can generate it, for example a class constant) and a dictionary for a payload. Also, while searching for payload keys for iOS system notifications is more or less easy, it could be much more cumbersome to let users know of your payloads’ keys, or, in case of small projects, you may have to resort to peek into the code generating the notification to know which keys to use when inspecting the payload.

Untyped payloads

I left this as a last point but this is actually my biggest issue with the notification center. I really dislike using dictionaries to store payloads. I think it encourages ignoring them altogether, since they’re mostly black boxes where there is little guarantee that we’ll find what we need to make good use of the notification in the first place, and discoverability as described in the previous point is poor at best. That’s why I’m just fine with empty notifications like UIApplicationDidBecomeActiveNotification and the like. I can just rely on the name and don’t care about looking into the payload anyway.

A different approach

The idea we came with is untypical in that it’s not easy to open source in the form of a library or the like, since it has a very small kernel that can be effectively reused, but relies on the app developer to be extended for each and every case to better adapt to your domain and usage.

Main characteristics

Our solution is composed of a MessageProducer protocol, implemented by a MessageBus class exposing a singleton for convenience (but that can be easily instantiated for small, single purpose message dispatchers), and a MessageConsumer protocol, that should be implemented by the listeners of the relevant events.

The main characteristics of our MessageBus are:

  • It’s async. You can dispatch messages and be sure that your code won’t be blocked by the consumers, even though they start long-running tasks in response to a specific message.

  • It doesn’t retain subscribers. As the modern NSNotificationCenter, our MessageBus has weak references to its subscribers, so you don’t have to unsubscribe, but you can, of course, in case you want to stop receiving messages even before the end of life of the object.

  • It’s typed. With this I mean that the messages you dispatch actually declare at compile time all the information they’ll carry with them at runtime. This is achieved through extensive use of enums, in a way that I didn’t encounter before and that I find really fits this use case.

  • It’s easy for the subscriber to listen to interesting events. You can just subscribe to the MessageBus and you’ll get notified of every incoming message, but since they’re enums it’s very easy to only actually respond to the ones you’re interested in.

  • It’s simple. The MessageBus it’s simple by choice, with no possibility to specify delivery queues or assign priorities to specific messages. It’s just a fire-and-forget.

  • It’s domain specific. As I’ve written before, the peculiar use of enums makes our MessageBus, and MessageProducer protocol in general, very domain specific. This is to stick to its very only purpose, that is to deliver domain specific messages to your app environment.

Meet the mighty switch statement

This is a snippet of how the usage of our MessageProducer might look like:

messageProducer.dispatchEvent(.Issue(issueElement, .Native(.Open)))

You can immediately see that we’re not dispatching an object, but a case of the enum MessageEvent that has to be populated by the app developer and that makes for an easy readable message where we’re passing along a model object and information about what kind of event it was.

This is a snippet of one of our consumers:

public func consumeEvent(event: MessageEvent) {
    switch event {
    case .ApplicationConfiguration(let configuration, .Loaded):
      //Do some stuff
    default: break
    }
  }

Here the consumer receives the message and switches it. We’re only interested in the “Application configuration loaded” event, that is nicely described through the enum you see above, and that passes along the configuration object as well.

The nice thing about the Swift switch statement is that it can not only switch enums, but nested enums too. And this comes really handy for our usage of them.

Indeed, the above consumer could also switch another case, that is .ApplicationConfiguration(_, .Error(let error)). This is the same top-most MessageEvent case as before, but with a different nested enum (.Error(ErrorType) instead of .Loaded).

This lets us only filter for the events we’re interested in, in a very granular way, and in a way that is supported by both the language and the compiler. Cool!

A real use case

Below you can find an example of some of the MessageEvent cases we defined in the Edition app. The complete list is much longer and more exhaustive, but it’s out of the scope of this post. It’s up to you to decide how deep and granular to go with your messages.

public enum MessageEvent {
  case Article(ArticleTreeElement, MessageEventType)
  case Image(MediaTreeElement, MessageEventType)
  case Video(MediaTreeElement, MessageEventType)
  case Application(ApplicationEventType)
  case ApplicationConfiguration(AppConfiguration, NativeEvent)
  case UserEvent(UserEventType)
  ...
}

public enum MessageEventType {
  case Native(NativeEvent)
  case UserInteraction(UserInteractionEvent)
}

public enum NativeEvent {
  case Open
  case Close
  case Loaded
  case Error(ErrorType)
}

public enum UserInteractionEvent {
  case Swipe
  case Tap(UserInteractionEventSource)
  case DoubleTap(UserInteractionEventSource)
}

public enum UserInteractionEventSource {
  case Area(CGRect)
  case Origin(CGPoint)
  case IgnoreSource
}

public enum ApplicationEventType {
  case Started
  case Terminated
  case Background
  case Foreground
}

public enum UserEventType {
  case LoggedIn
  case LoggedOut
  case SubscriptionStatusUpdated
  case InfoUpdated
  case Registered
  case BoughtProduct(Product)
  case RegisteredDevice
}

[...]

The “schema” lets us dispatch events like the following:

messageBus.dispatchEvent(.UserGeneratedEvent(.Article(articleElement, .Favorited(wasFavorited))))

where .UserGeneratedEvent is a MessageEvent case we mostly (but not only) use for tracking. This way we also don’t have a single place/class where tracking happens, but one MessageConsumer for every tracking system we implement. Adding a new one or removing one we don’t want to use anymore is a no-brainer and has a near-zero impact on the codebase.

Conclusion

Here you can find the complete code for the MessageProducer, MessageBus and MessageConsumer protocols.

I hope you will find this alternative approach to NSNotificationCenter and the associated usage of Swift enums interesting and useful for your next project!

Let me know here or on Twitter what do you think and more importantly your suggestions to improve it!