Coordinator Pattern
Coordinator is not a general design pattern to solve general programming problems. It is a combination of Factory and Delegation design patterns to separate creation and navigation operations from other responsibilities of view controllers.
Coordinator patterns was mentioned for the first time by the Soroush Khanlou. As Khanlou says:
“A coordinator is an object that bosses one or more view controllers around. Taking all of the driving logic out of your view controllers, and moving that stuff one layer up is gonna make your life a lot more awesome.”
If you google the Coordinator, you will see that there are different implementations for the Coordinator pattern. Some of them are simpler, some others more complex.
Isolating view controllers from creation and navigation logic is a great idea. It makes the view controllers more single responsible, reusable, and testable. Hence, it is necessary to have a clear approach to achieve that, especially in medium and large-scale projects.
As far as I understood, Coordinators have some problems. To learn about them, read this post. But what I do not like about Coordinator most of all is the fact that it does not follow a simple approach. It is, to some extent, difficult to understand and maintain, at least for me. So the Coordinator pattern has never been my choice.
Solution: Router
Based on Clean Architecture, we have an interactor and a presenter for a typical view controller. Interactor contains all the business login and presenter is a place for writing codes that convert data models to view models to be easy to represent by view controllers. There are some protocols as boundaries between view controller, interactor, and presenter to have polymorphism. I ignore them here for the sake of simplicity.
Arrows show data flows, not dependencies.
The main interactor uses some services to fulfill its needs, services like database, networking, etc. and one of these services is routing service. Routing service is responsible for navigating and sending requests from interactor to other view controllers.
Router Protocol
Router protocol is not a specific protocol with a special definition. It acts as a polymorphic interface to make loose coupling between the interact and the router. Its definition depends on the requirements of the interactor. That means interactor sends special kind of requests to the router that require showing other view controllers to be executed. The login request is a simple example of an operation that needs to show another view controller to perform. So the interactor sends the login request to its Router.
Router Contents
Each Router has a weak reference to its own view controller. The main router of the app has also a reference to a window to show the root view controller.
Router Responsibilities
It has four responsibilities:
- Instantiate a target view controller.
- Navigate to the target view controller.
- Call a method on the view controller or pass some data to it.
- Disappear its view controller.
Instantiation Target View Controller
Normally it is a heavy task that must be outsourced to the target view controller factory.
Navigation and Method Call
Router is the one who knows how to navigate to the target view controller.
Disappear
Router knows also how to disappear its view controller.
Sample Project
AppRouter
The AppRouter code is like this. We instantiate and pass the app window in the app scene delegate’s scene(_ scene:, willConnectTo:, options:) method.
class AppRouter {
let window: UIWindow?
init(window: UIWindow?) {
self.window = window
}
func start() {
let rootViewController = MainViewControllerFactory().build()
let rootNavigationController = UINavigationController(rootViewController: rootViewController)
window?.rootViewController = rootNavigationController
window?.makeKeyAndVisible()
}
}
MainRouterProtocol
MainRouterProtocol is the router of the main interactor. The main interactor declares its requirements there.
public protocol MainRouterProtocol {
func order(message: String, completion: @escaping (Result) -> Void)
}
MainRouter
MainRotuer conforms to the MainRouterPrototol and has a reference to the main view controller. Note this reference must be weak, otherwise, there will be a strong reference cycle.
class MainRouter: MainRouterProtocol {
weak var viewController: UIViewController?
func order(message: String, completion: @escaping (Result<String, Error>) -> Void) {
let orderViewController = OrderViewControllerFactory.build()
orderViewController.text = message
orderViewController.completion = completion
viewController?.show(orderViewController, sender: self)
}
}
OrderRouter
OrderRouter is exactly the same as MainRouter but it has also a dismiss method to disappear the Order view controller.
class OrderRouter: OrderRouterProtocol {
weak var viewController: UIViewController?
func login(completion: @escaping (Bool) -> Void) {
//
}
func dismiss() {
viewController?.navigationController?.popViewController(animated: true)
}
}
Benefits
- View controllers with a higher degree of single responsibility that are more testable and reusable.
- Not fighting or duplicating the UIKit structure.
- An easy-to-understand, maintainable and extendable approach.
Finally, it was my solution to the navigation separation problem.
Please leave comments below if you think this is not perfect.
Thanks,