Codementor Events

Observing and broadcasting

Published Jun 26, 2018Last updated Jul 01, 2019

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 Notification.Name { // 2

  static let LTHPostReceived = Notification.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.

You can find more articles like this on my blog, or you can subscribe to my monthly newsletter. Originally published at https://rolandleth.com.

Discover and read more posts from Roland Leth
get started
post commentsBe the first to share your opinion
Show more replies