📜 ⬆️ ⬇️

Architecture creation: working with iOS Coordinator pattern


( Illustration )

Each team sooner or later begins to think about introducing their own architectural approaches, and many copies were broken about it. So at Umbrella IT we always wanted to work with flexible tools, so that the formation of the architecture was not something painful, and the problems of navigation, mock-files, isolation and testing ceased to be something scary, something that sooner or later hanging over the overgrown project. Fortunately, we are not talking about a new “exclusive” architecture with an elaborate abbreviation. It must be admitted that the currently existing popular architectures (MVP, MVVM, VIPER, Clean-swift) cope with their tasks, and complexity can be caused only by the wrong choice and the wrong use of one or another approach. However, within the framework of the adopted architecture, it is possible to use various patterns, which will make it possible to achieve the very, almost mythical indicators: flexibility, isolation, testability, reuse.

Of course, applications are different. If the project contains only a few screens that are connected in series, then there is no particular need for complex interactions between modules. It is quite possible to get along with the usual segue links, spicing it all up with the good old MVC / MVP. And although architectural snobbery sooner or later overcomes every developer, the implementation must be commensurate with the goals and complexity of the project. And so, if the project assumes a complex structure of screens and various states (authorization, Guest mode, offline, roles for users, etc.), then a simplified approach to architecture will certainly play a cruel joke: a lot of dependencies, unclear and expensive data transfer between screens and states, problems with navigation and, most importantly, all this will not have any flexibility and reusability, solutions will be tightly fused into the project and screen A will always open screen B. Attempts to change will lead to painful refactory ngam, during which it is so easy to make mistakes and break things that used to work. In the example below, we describe a flexible way of organizing the operation of an application that has two states: the user is not authorized and should be sent to the authorization screen, the user is authorized and a certain Main-screen should be opened.
')

1. Implementation of the main protocols


First we need to implement the base. It all starts with the Coordinatable, Presentable, Routable protocols:

protocol Coordinatable: class { func start() } protocol Presentable { var toPresent: UIViewController? { get } } extension UIViewController: Presentable { var toPresent: UIViewController? { return self } func showAlert(title: String, message: String? = nil) { UIAlertController.showAlert(title: title, message: message, inViewController: self, actionBlock: nil) } } 

In this example, showAlert is just a convenient method for invoking a notification, which is in the UIViewController extension.

 protocol Routable: Presentable { func present(_ module: Presentable?) func present(_ module: Presentable?, animated: Bool) func push(_ module: Presentable?) func push(_ module: Presentable?, animated: Bool) func push(_ module: Presentable?, animated: Bool, completion: CompletionBlock?) func popModule() func popModule(animated: Bool) func dismissModule() func dismissModule(animated: Bool, completion: CompletionBlock?) func setRootModule(_ module: Presentable?) func setRootModule(_ module: Presentable?, hideBar: Bool) func popToRootModule(animated: Bool) } 

2. Creating a coordinator


From time to time there is a need to change the application screens, which means it will be necessary to implement the test layer without downcast, and also without violating the SOLID principles.

Let's start the implementation of the coordinate layer:



After starting the application, the AppCoordinator method should be called, which determines which flow should be started. For example, if a user is registered, then the application's flow should be started, and if not, then the authorization flow. In this case, MainCoordinator and AuthorizationCoordinator are required. We will describe the coordinator for authorization, all other screens can be created in the same way.

First you need to add an output coordinator so that he can have a connection with the higher level coordinator (AppCoordinator):

 protocol AuthorizationCoordinatorOutput: class { var finishFlow: CompletionBlock? { get set } } final class AuthorizationCoordinator: BaseCoordinator, AuthorizationCoordinatorOutput { var finishFlow: CompletionBlock? fileprivate let factory: AuthorizationFactoryProtocol fileprivate let router : Routable init(router: Routable, factory: AuthorizationFactoryProtocol) { self.router = router self.factory = factory } } // MARK:- Coordinatable extension AuthorizationCoordinator: Coordinatable { func start() { performFlow() } } // MARK:- Private methods private extension AuthorizationCoordinator { func performFlow() { //:- Will implement later } } 



As shown above, we have an Authorization coordinator with a router and a module factory. But who calls the start () method and when?
Here we need to implement AppCoordinator.

 final class AppCoordinator: BaseCoordinator { fileprivate let factory: CoordinatorFactoryProtocol fileprivate let router : Routable fileprivate let gateway = Gateway() init(router: Routable, factory: CoordinatorFactory) { self.router = router self.factory = factory } } // MARK:- Coordinatable extension AppCoordinator: Coordinatable { func start() { self.gateway.getState { [unowned self] (state) in switch state { case .authorization: self.performAuthorizationFlow() case .main: self.performMainFlow() } } } } // MARK:- Private methods func performAuthorizationFlow() { let coordinator = factory.makeAuthorizationCoordinator(with: router) coordinator.finishFlow = { [weak self, weak coordinator] in guard let `self` = self, let `coordinator` = coordinator else { return } self.removeDependency(coordinator) self.start() } addDependency(coordinator) coordinator.start() } func performMainFlow() { // MARK:- main flow logic } 

From the example, you can see that AppCoordinator has a router, a coordinator factory and the state of the entry point for AppCoordinator, whose role is to determine the start of the flow of the application.

 final class CoordinatorFactory { fileprivate let modulesFactory = ModulesFactory() } extension CoordinatorFactory: CoordinatorFactoryProtocol { func makeAuthorizationCoordinator(with router: Routable) -> Coordinatable & AuthorizationCoordinatorOutput { return AuthorizationCoordinator(router: router, factory: modulesFactory) } } 

3. Implementation of the factory coordinators


Each coordinator is initialized with a router and a module factory. Moreover, each coordinator must inherit from the base coordinator:

 class BaseCoordinator { var childCoordinators: [Coordinatable] = [] // Add only unique object func addDependency(_ coordinator: Coordinatable) { for element in childCoordinators { if element === coordinator { return } } childCoordinators.append(coordinator) } func removeDependency(_ coordinator: Coordinatable?) { guard childCoordinators.isEmpty == false, let coordinator = coordinator else { return } for (index, element) in childCoordinators.enumerated() { if element === coordinator { childCoordinators.remove(at: index) break } } } } 

BaseCoordinator is a class containing an array of child coordinators and two methods: Delete and Add coordinator dependency.

4. Configure AppDelegate


Now let's see what UIApplicationMain looks like:

 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var rootController: UINavigationController { window?.rootViewController = UINavigationController() window?.rootViewController?.view.backgroundColor = .white return window?.rootViewController as! UINavigationController } fileprivate lazy var coordinator: Coordinatable = self.makeCoordinator() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { coordinator.start() return true } } // MARK:- Private methods private extension AppDelegate { func makeCoordinator() -> Coordinatable { return AppCoordinator(router: Router(rootController: rootController), factory: CoordinatorFactory()) } } 

As soon as the delegate's didFinishLaunchingWithOptions method is called, the Start () method of the AppCoordinator is called, which determines the further logic of the application.

5. Creating a screen module


To demonstrate what happens next, let's go back to the AuthorizationCoordinator and implement the performFlow () method.

First we need to implement the AuthorizationFactoryProtocol interface in the ModulesFactory class:

 final class ModulesFactory {} // MARK:- AuthorizationFactoryProtocol extension ModulesFactory: AuthorizationFactoryProtocol { func makeEnterView() -> EnterViewProtocol { let view: EnterViewController = EnterViewController.controllerFromStoryboard(.authorization) EnterAssembly.assembly(with: view) return view 

Calling any method for a module factory usually means initializing a ViewController from a storyboard and then linking all the necessary components of this module within a specific architecture (MVP, MVVM, CleanSwift).

After the necessary preparations, we can implement the performFlow () method of the AuthorizationCoordinator.
The start screen within this coordinator is EnterView.
In the performFlow () method, using the module factory, the creation of a ready module for a given coordinator is called, then the logic of processing closures that our view controller causes at one time or another is implemented, then this module is set up by the router in the navigation screen stack:

 private extension AuthorizationCoordinator { func performFlow() { let enterView = factory.makeEnterView() finishFlow = enterView.onCompleteAuthorization enterView.output?.onAlert = { [unowned self] (message: String) in self.router.toPresent?.showAlert(message: message) } router.setRootModule(enterView) } } 




Despite the seeming complexity in some places, this pattern is ideal for working with mock files, allows you to completely isolate the modules from each other, and also abstracts us from UIKit, which is well suited for full coverage of tests. At the same time, the Coordinator does not impose strict requirements on the application architecture and is only a handy addition, structuring navigation, dependencies and data flows between modules.

Link to github , which contains a demo based on Clean architecture and convenient Xcode Template to create the necessary architectural layers.

Source: https://habr.com/ru/post/426507/


All Articles