Using Channels for Data Flow in Swift đź“»
An alternative to delegation and NotificationCenter
Apple frameworks use delegation and observer pattern (NotificationCenter) heavily to pass information around. Although there is nothing wrong about these patterns, the actual implementation always looked a bit inconsistent to me.
Let’s look at the basic traits of these patterns first:
- Delegation: Supports 1-to-1, two-way communication.
- Observer: Supports 1-to-many, one-way communication.
Let’s look at some UITableViewDelegate
methods.
optional func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
UITableView
: SenderUITableViewDelegate
(MostlyUIViewController
): Receiver
This method is a good example of 1-to-1, two-way communication.
Since the table view requires height to be returned by the controller, communication is two-way and it can’t be 1-to-many. (Otherwise, we wouldn’t be able to decide which returned height value should be used.)
However, the following method does not return anything.
optional func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
Here, sender only notifies the receiver. Therefore, the communication is one-way and can either be 1-to-1 or 1-to-many.
In this case, is delegation a good choice?
- Maybe…What happens if we need to notify more than one component?
- We can’t.What if we don’t care about row selection but want to provide row height ourselves? Do we still need to provide empty implementations?
- If this was a pure Swift protocol, yes, we would need empty implementations in place for each delegate method we don’t care about, which is pretty annoying. SinceUITableViewDelegate
is inherited from Objective-C, we can mark methods as optional for the sake of backwards compatibility. But Swift disables this “feature” for a reason. Having huge protocols violates interface segregation principle and makes them unreadable. Table views are a huge part of my life as a developer but I still can’t remember what’s defined inUITableViewDelegate
andUITableViewDataSource
protocols because the list is way too long.
So… Why don’t we use observer pattern for every one-way action? We could provide the same functionality with more flexibility and it would also clear up most methods in huge delegate protocols.
An Imaginary Problem
We have a settings page which features theme selection. We want to update our home screen whenever the theme changes.
Solution #1: Using Delegation
This solution works but is it flexible enough? What happens if we have 5 tabs to be updated with this theme change?
Solution #2: Using NotificationCenter
Now we support 1-to-many communication, but we doubled the number of lines, right? This is the main problem with NotificationCenter
. It is powerful but not very handy.
Solution #3: Using Channels
What is a channel?
It’s nothing new. Channel is an observer pattern implementation which provides a much better API than NotificationCenter
.
- Create a channel:
enum Message {
case themeDidChange(Theme)
}let channel = Channel<Message>()
- Subscribe to it:
channel.subscribe(self) { message in
// Handle message here.
}
- Broadcast a message:
channel.broadcast(.themeDidChange(.light))
So using a channel, our solution would look like the following:
Final Words
- We can standardize data flow in our code by adapting channels for one-way communication and delegation for two-way communication.
- We can finally stop using
NotificationCenter
. - Channel implementation is less than 100 lines of code and now is a part of Lightning framework.
Thanks for scrolling all the way!
Your opinion matters! Please let me know what you think and help spread the word. ❤️👏