Lightweight Design Patterns in iOS (Part 3) - Coordinator
Table of Contents
Lightweight Design Patterns in iOS (Part 3) - Coordinator
Table of Contents
Design Patterns are part of Mobile Development for a while now and a revolution towards Quality Assurance is on. Such patterns have become a standard among the community, yet implementing an academic one might come with a price โ complexity.
Because some Design Patterns solve very complex problems, they often result in a complicated implementation as well. So complex sometimes, that we tend to use heavy Third Party Frameworks, even for simple use cases.
The time has come to combine quality and simplicity.
๐ฌ Hi there, Iโm Jean!
Previously on the series Lightweight Design Patterns in iOS, we have covered the case of the Presenter Pattern or how to implement a lightweight presentation layer! ๐บ
In this episode, we will take a quick look at the Coordinator Pattern, the problem it solves as well as how to implement it in a simple and yet powerful manner! ๐ฆ
The role of the Coordinator is to coordinate the navigation between screens of the same flow. ๐บ
This means in Mobile Development: defining a user flow! ๐ฒ
In iOS, the Storyboard is already providing this feature but it also comes with considerable drawbacks, such as loading time and testability. ๐
The Coordinator allows you to separate the Navigation logic from the ViewController and define your screen flow directly in code! ๐ฏ
Letโs now look at an example! ๐
We have an app, that displays a list of โthingsโ and tapping on one of them opens a detail screen with more information concerning that item. Easy. ๐
Our navigation is going to look very concise: we will first start with a list of things and later on eventually open something. This flow definition is nothing else than our CoordinatorProtocol! ๐
protocol CoordinatorProtocol: class {
func start()
func open(_ something: Something)
}
Looking good! ๐
Letโs now implement the actual Coordinator!
final class Coordinator {
private let window: UIWindow
let navigationController = UINavigationController()
init(window: UIWindow) {
self.window = window
self.window.rootViewController = navigationController
self.window.makeKeyAndVisible()
}
}
As you can see, we need to inject a UIWindow in the init method, in order to make the navigationController the rootViewController of this window. ๐ฑ
Now, implementing the CoordinatorProtocol will only consist of instantiating the ViewControllers with their dependencies injected and push them to the NavigationController! ๐ฏ
extension Coordinator: CoordinatorProtocol {
func start() {
let viewController = MainViewController()
viewController.viewModel = MainViewModel()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}
func open(_ something: Something) {
let viewController = SomethingViewController()
viewController.viewModel = SomethingViewModel()
viewController.coordinator = self
navigationController.pushViewController(viewController, animated: true)
}
}
And thatโs it! Our flow is now implemented, and can be started/continued from anywhere in the App! ๐
Letโs start with the AppDelegate!
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var coordinator: CoordinatorProtocol!
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
coordinator = Coordinator()
return true
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
coordinator.start()
return true
}
}
Nice and simple! Now letโs look at the MainViewController!
final class MainViewController: UITableViewController {
weak var coordinator: CoordinatorProtocol?
var viewModel: SomethingViewModel!
...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let something = viewModel.somethings[indexPath.row]
coordinator?.open(something)
}
...
}
Thatโs it! The entire Navigation Logic has just been delegated to an independent layer! ๐
Unit testing the navigation has never been easier!
Before writing any test, letโs implement a CoordinatorMock first! ๐
final class CoordinatorMock: CoordinatorProtocol {
private(set) var isStarted = false
private(set) var isSomethingOpened = false
func start() {
isStarted = true
}
func open(_ something: Something) {
isSomethingOpened = true
}
}
As you can see, we are just implementing the CoordinatorProtocol and toggle the respective boolean properties whenever a method is called. ๐
Simple right?! Well, look at how easy testing now is! ๐ช
Letโs start with the AppDelegate and verify the start method is called!
final class AppDelegateTest: XCTestCase {
var sut: AppDelegate!
override func setUp() {
super.setUp()
sut = AppDelegate()
}
func testAppDelegateStartsCoordinatorSuccessfully() {
let coordinator = CoordinatorMock()
sut.coordinator = coordinator
_ = sut.application(UIApplication.shared, didFinishLaunchingWithOptions: [:])
XCTAssertTrue(coordinator.isStarted)
}
}
Marvellous! ๐
And now with the MainViewController, letโs make sure the open function is called!
final class MainViewControllerTest: XCTestCase {
var sut: MainViewController!
override func setUp() {
super.setUp()
sut = MainViewController()
sut.viewModel = SomethingViewModel()
_ = sut.view
}
func testMainViewControllerOpensSomethingSuccessfully() {
let coordinator = CoordinatorMock()
sut.coordinator = coordinator
sut.tableView(sut.tableView, didSelectRowAt: IndexPath(item: 0, section: 0))
XCTAssertTrue(coordinator.isSomethingOpened)
}
}
And boom! ๐ฅ
We just made a lightweight Coordinator which is even testable! ๐
Worth it? Did that help you see what problems the Coordinator Pattern is trying to solve? How to make such a pattern lightweight?
If so, follow me on Twitter, Iโll be happy to answer any of your questions and youโll be the first one to know when a new article comes out! ๐
See you next week, for the Part 4 of my series Lightweight Design Patterns in iOS!
Bye bye! ๐