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:
- 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. - Consumers of the API are required to unregister when they are no longer interested in notifications.
- 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
!