:  ~ 3 min read

Observing and broadcasting

The usual solution to observe and broadcast is to use NotificationCenter:

final class Post { // 1

  	var title: String
	var body: String

	init(title: String, body: String) {
		self.title = title
		self.body = body
	}

}

extension NSNotification.Name { // 2

	static let LTHPostReceived = NSNotification.Name(rawValue: "com.rolandleth.com.postReceivedNotification")

}

final class PostCreationController: UIViewController { // 3

	private let post: Post

	// [...]

	private func savePost() { // 4
		// [...]

		let userInfo = ["post": post] // 5
		let notification = Notification(name: .LTHPostReceived, object: nil, userInfo: userInfo) // 6

		NotificationCenter.default.post(notification) // 7
	}

	// [...]

}

final class FeedViewController: UIViewController { // 8

	// [...]

	override func viewDidLoad() {
		super.viewDidLoad()

		NotificationCenter.default.addObserver(self, // 9
															selector: #selector(postReceived),
															name: .LTHPostReceived,
															object: nil)
	}

	@objc
	private func postReceived(from notification: Notification) { // 10
		guard let post = notification.userInfo?["post"] as? Post else { return } // 11

		// Do something with post.
	}

	// [...]

}

Let's use a Post (1) as an example.

First of all, we need a Notification.Name extension (2) to create a custom notification name to pass it around.

Next, let's imagine a controller where we create a new post (3): in its save method (4), we have to create a userInfo dictionary (5), a Notification (6) and broadcast it (7).

Finally, let's imagine a controller to display a feed of posts (8): we need to add ourselves as an observer somewhere (9) and handle the notification when we receive it (10). The biggest downside here is that we need to try and extract our Post from the userInfo dictionary, found under the post key (which is a plain string, leaving room for errors), and only then can we use it.

A lot of boilerplate code, not quite safe and not quite pretty to use. I'm sure we can do better, don't you think? Let's start with a broadcaster:

final class GlobalBroadcaster {

	private var listenersTable: NSHashTable<AnyObject> = .weakObjects() // 1


	// MARK: - Adding listeners

	func addListener(_ object: AnyObject) { // 2
		listenersTable.add(object)
	}


	// MARK: - Helpers

	private func filteredListeners<T>() -> [T] { // 3
		return listenersTable.allObjects.compactMap { $0 as? T }
	}

	private func keyboardChanged(with notification: Notification) {
		// Some keyboard handling logic.
	}

	private func setKeyboardObserver() { // 4
		NotificationCenter.default.addObserver(forName: .UIKeyboardWillShow, object: nil, queue: nil) { [weak self] notification in
			self?.keyboardChanged(with: notification)
		}

		NotificationCenter.default.addObserver(forName: .UIKeyboardWillHide, object: nil, queue: nil) { [weak self] notification in
			self?.keyboardChanged(with: notification)
		}

	}


	// MARK: - Init

	init() {
		setKeyboardObserver()
	}

	// static let shared = GlobalBroadcaster() // 5

}

let Broadcaster = GlobalBroadcaster() // 6

The backbone of our broadcaster is the array of listeners (1), backed by an NSHashTable<AnyObject>weakObjects(). An NSHashTable is a collection similar to a Set — we want the objects inside it to be unique — and the .weakObjects initializer means the NSHashTable will store weak references to its contents and no retain cycles will occur—objects will be deallocated properly, instead of being kept alive indefinitely.

Next we need a method to add listeners (2), instead of exposing the listenersTable property. When we broadcast something, we will be interested in only one type of listeners, so (3) is a helper to filter only what we need — we'll see in just a bit how this plays out. This approach still lets us use usual NotificationCenter actors (4), but gives us a chance to parse or manipulate objects before exposing them to our app.

Finally, we'll be creating a global variable, so our Broadcaster can be available everywhere (6); or we can use a static property on GlobalBroadcaster (5), in which case the class itself could be named Broadcaster — I just like to type a bit less.

Next up, listeners. How do we listen and broadcast events? With protocols:

protocol PostCreationListener {

	func handlePostCreationBroadcast(with post: Post)

}

// Just an example.
protocol LoginListener {

	func handleUserLoginBroadcast(with user: User)
	func handleUserLogoutBroadcast()

}

final class FeedController: UIViewController {

	// [...]

	override func viewDidLoad() {
		super.viewDidLoad()

		Broadcaster.addListener(self) // 1
	}

	// [...]

}

extension FeedController: PostCreationListener { // 2

	func handlePostCreationBroadcast(with post: Post) { // 3
		// Do something with post.
	}

}

We conform FeedController to PostCreationListener (2), add ourselves as a listener (1) and implement the required method (3) — we'll be able to directly use our post, without String keys and casting.

Finally, we also need to broadcast events, right?

final class GlobalBroadcaster {

	// [...]

	func postCreated(_ post: Post) { // 1
		let listeners: [PostCreationListener] = filteredListeners() // 2

		listeners.forEach {
			$0.handlePostCreationBroadcast(with: post) // 3
		}
	}

	// [...]

}

final class PostCreationController: UIViewController {

	// [...]

	private func savePost() {
		Broadcaster.postCreated(post) // 4
	}

	// [...]

}

We'll add a new method on our Broadcaster (1) that uses our previously mentioned filter method: since we declare listeners (2) as [PostCreationListener], the compiler can infer the filteredListeners' T return value. We then have to iterate through all listeners and call handlePostCreationBroadcast:. Lastly, postCreated will have to be called from our PostCreationController (4).

It might seem a bit more code, but we now have type-safety, an easy way to extend our listeners via protocols and a central place where we parse or manipulate objects before exposing them to our app.

Let me know what you think @rolandleth.