-
Notifications
You must be signed in to change notification settings - Fork 4
Description
A coordinator is an architectural component that rose into fame the last couple of years or so. It primarily solves the problem of tight coupling between view controllers regarding their presentation and dismissal. You can learn more about it here.
To summarize, coordinators solve the tight coupling problem by extracting destination view controller creation and navigation implementation details from a source view controller to a coordinator object that manages that flow. So, let's have a simple example.
We have an app that is composed of a view controller (let's call it MainViewController) that is embedded in a UINavigationController, and has a button that pushes a SettingsViewController when tapped.
Coordinators avoid doing this:
class MainViewController {
@objc func showSettingsButtonTapped(_ sender: Any?) {
let settingsVC = SettingsViewController()
navigationController?.pushViewController(settingsVC, animated: true)
}
}And do this instead:
class MainViewController {
weak var coordinator: Coordinator?
@objc func showSettingsButtonTapped(_ sender: Any?) {
coordinator?.showSettings()
}
}Without writing it explicitly, the code for creating and pushing SettingsViewController went to the coordinator's showSettings().
Covert Patterns
The goal of this article is doing the above exactly without explicitly spelling out "Coordinator". But let me first show what value I find in doing that.
I like to think of a pattern as a form that code evolves into while seeking a set of goals and respecting a set of principles. I find elegance in code developing healthy patterns without expanding the code's vocabulary as possible. Can we achieve the same goals without the "Coordinator" word (or any equivalent)? Let's see.
Let's start with the above code snippet:
class MainViewController {
weak var coordinator: Coordinator?
@objc func showSettingsButtonTapped(_ sender: Any?) {
coordinator?.showSettings()
}
}Let's replace the coordinator dependency with a closure:
class MainViewController {
var showSettings: (() -> Void)?
@objc func showSettingsButtonTapped(_ sender: Any?) {
showSettings?()
}
}Good. Now, we need to know how the closure is passed and where it's implemented. Coordinators often rely on a container view controller, commonly a UINavigationController. Why not directly use a UINavigationController subclass then? Let's try that.
class MainNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
let mainVC = MainViewController()
mainVC.showSettings = { [weak self] in
self?.pushViewController(SettingsViewController(), animated: true)
}
setViewControllers([mainVC], animated: false)
}
}Here we subclassed our root navigation controller, and supplied MainViewController's showSettings() implementation. I find this simpler while maintaining the same gains.
Actually I think there's an advantage to this approach over coordinators. If you notice, while using coordinators, MainViewController could call any method it wants from the coordinator property, even if it's irrelevant. Supplying a single closure to call looks much cleaner to me.
Have a look at a sample project where this is implemented. The sample also shows the case of a child flow that would be implemented by so called "child coorindators".
Final Word
As you see, this was an opinionated article. We don't have to agree on this. Feel free to leave your feedback. Thanks for reading.