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! π