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 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.