My book on iOS interface design, Design Teardowns: Step-by-step iOS interface design walkthroughs is now available!

A better NSNotificationCenter

NSNotificationCenter has been around for a long time. It let's you post arbitrary notifications and decouples the source of the action from the destination. There are a few flaws though:

  1. The notification name is a String type which is subject to typing errors or enforces a design pattern where you would declare constants to avoid this.
  2. Consumers of the API are required to unregister when they are no longer interested in notifications.
  3. Parameters are passed via a userInfo dictionary and type information is lost in the process.

We can solve problems 1 and 3 easily but simply requiring users to declare a protocol. This gives us a name to refer to this bit of functionality and also allows consumers to define the requirements (read parameters) of the notification.

The delegate design pattern makes uses of protocols but is usually a one-to-one relationship between the delegate and the consumer (instead of enabling action at a distance.) The second problem we can solve by storing the observers weakly via NSHashTable.

Here's how we would use such a solution:

protocol TestProtocol {  
  func hello()
}

We'll declare the protocol that defines the functionality we want to expose.

class TestClass: TestProtocol {  
  let name: String

  init(name: String) {
    self.name = name
  }

  func hello() {
    print("hello \(name)")
  }
}

Next, we implement the protocol in a class.

let object = TestClass(name: "First")  
let object2 = TestClass(name: "Second")  

Suppose we instantiate several instances of the class.

NotificationManager.sharedManager.add(TestProtocol.self, observer: object)  
NotificationManager.sharedManager.add(TestProtocol.self, observer: object2)  

We'll subscribe to notifications via the func add() method.

NotificationManager.sharedManager.make(TestProtocol.self) { (item) -> Void in  
  item.hello()
}

Here's how we can fire off notifications. We call the methods in the protocol inside of a closure and it gets fired on all observers.

hello First  
hello Second  

The output shows the desired result.

Let's explore how we can build such a solution:

class NotificationManager  
{
  static let sharedManager = NotificationManager()
  var observersMap = [String:NSHashTable]()
}

We start by having a similar way to access a singleton if necessary. Next, we need a way to add observers to our dictionary, observersMap.

func add<T>(type: T.Type, observer: T) {  
  let typeString = "\(T.self)"
  let observers: NSHashTable

  // get the weak set of observers matching this type
  if let set = observersMap[typeString] {
    observers = set
  } else {
    observers = NSHashTable.weakObjectsHashTable()
    observersMap[typeString] = observers
  }

  observers.addObject(observer as? AnyObject)
}

We can do this by writing a generic method. It takes as parameters a type T and an observer conforming to T. Next, we produce a String matching the name of T because the key to a dictionary needs to be Hashable and String conforms to that.

We check to see if we already have a NSHashTable matching that type. If we don't we'll just instantiate one and add it to the dictionary. Finally we add our observer to it. Note that our observer needs to be an object, because the hash table is an Objective-C class.

Now we're ready to write our func make() method. This method makes the observers registered perform a given closure. We've specified the closure to be @noescape. This means it will not out-live the func make() method and we won't have to worry about capturing references.

func make<T>(type: T.Type, @noescape closure: T -> Void) {  
  let typeString = "\(T.self)"
  if let set = observersMap[typeString] {
    for observer in set.allObjects {
      closure(observer as! T)
    }
  }
}

Let me know what you think and if this is a better solution than NSNotificationCenter!