
(Иллюстрация)
Каждая команда рано или поздно начинает думать о внедрении собственных архитектурных подходов, и немало было об это копий сломано. Вот и мы в Umbrella IT всегда хотели работать с гибкими инструментами, чтобы формирование архитектуры не было чем-то болезненным, и проблемы навигации, mock-файлов, изолированности и тестирования перестали быть чем-то страшным, чем-то таким, что рано или поздно нависает над разросшимся проектом. К счастью, речь не идет о новой «эксклюзивной» архитектуре с вычурным названием-аббревиатурой. Надо признать, что существующие на данный момент популярные архитектуры (MVP, MVVM, VIPER, Clean-swift) справляются со своими задачами, и сложности может вызвать лишь неправильный выбор и неправильное использование того или иного подхода. Однако и в рамках принятой архитектуры можно использовать различные паттерны, что позволит добиться тех самых, почти мифических показателей: гибкость, изолированность, тестируемость, переиспользование.
Безусловно, приложения бывают разные. Если проект содержит в себе лишь несколько экранов, которые связаны последовательно, то нет особой необходимости в сложных взаимодействиях между модулями. Вполне можно обойтись обычными segue-связями, приправив все это старым добрым MVC/MVP. И хотя архитектурный снобизм рано или поздно одолевает каждого разработчика, все-таки реализация должна быть соизмерима целям и сложности проекта. И вот, если в проекте предполагается сложная структура экранов и различные состояния (авторизация, режим Гостя, офлайн, роли для пользователей и т.д.), то упрощенный подход к архитектуре непременно сыграет злую шутку: множество зависимостей, неочевидный и дорогой переброс данных между экранами и состояниями, проблемы с навигацией и главное — никакой гибкости и переиспользуемости у всего этого не будет, решения будут намертво вплавляться в проект и экран А всегда будет открывать экран B. Попытки же изменения приведут к мучительным рефакторингам, во время которых так легко создавать ошибки и ломать то, что раньше работало. В примере ниже мы опишем гибкий способ организации работы приложения, которое имеет два состояния: пользователь не авторизован и его следует направить на экран авторизации, пользователь авторизован и следует открыть некий Main-экран.
1. Реализация основных протоколов
Для начала нам необходимо реализовать базу. Все начинается с протоколов Coordinatable, Presentable, Routable:
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) } }
В данном примере showAlert — это просто удобный метод для вызова уведомления, который находится в расширении UIViewController.
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. Создание координатора
Время от времени возникает необходимость изменения экранов приложения, а значит будет необходимо реализовать тестируемый слой без downcast’a, а также без нарушения SOLID-принципов.
Приступим к реализации координатного слоя:

После запуска приложения должен быть вызван метод AppCoordinator’а, который определяет, какой flow следует запустить. Например, если пользователь зарегистрирован, то следует запустить flow приложения, а если нет, то flow авторизации. В данном случае необходимы MainCoordinator и AuthorizationCoordinator. Мы опишем именно координатор для авторизации, все прочие экраны могут быть созданы аналогичным образом.
Для начала необходимо добавить output координатору, чтобы тот мог иметь связь с вышестоящим координатором (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 } }

Как показано выше, у нас есть Authorization координатор с роутером и фабрикой модулей. Но кто и когда вызывает метод start()?
Здесь нам необходимо реализовать 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 }
Из примера можно видеть, что у AppCoordinator’a есть роутер, фабрика координаторов и состояние точки входа для AppCoordinator’a, ролью которого является определение старта flow у приложения.
final class CoordinatorFactory { fileprivate let modulesFactory = ModulesFactory() } extension CoordinatorFactory: CoordinatorFactoryProtocol { func makeAuthorizationCoordinator(with router: Routable) -> Coordinatable & AuthorizationCoordinatorOutput { return AuthorizationCoordinator(router: router, factory: modulesFactory) } }
3. Реализация фабрики координаторов
Каждый из координаторов инициализируется с роутером и фабрикой модулей. Более того, каждый из координаторов должен наследоваться от базового координатора:
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 — класс, в котором содержится массив дочерних координаторов и два метода: Удалить и Добавить зависимость координатора.
4. Настройка AppDelegate
Теперь посмотрим, как выглядит UIApplicationMain:
@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()) } }
Как только вызовется метод делегата didFinishLaunchingWithOptions — вызывается метод start() у AppCoordinator’а, который определит дальнейшую логику приложения.
5. Создание модуля экрана
Для демонстрации того, что же происходит дальше, вернемся назад к AuthorizationCoordinator и реализуем метод performFlow().
Для начала нам следует реализовать интерфейс AuthorizationFactoryProtocol в классе ModulesFactory:
final class ModulesFactory {} // MARK:- AuthorizationFactoryProtocol extension ModulesFactory: AuthorizationFactoryProtocol { func makeEnterView() -> EnterViewProtocol { let view: EnterViewController = EnterViewController.controllerFromStoryboard(.authorization) EnterAssembly.assembly(with: view) return view
Под вызовом любого метода у фабрики модулей, как правило, подразумевается инициализация ViewController’a из сториборда, а затем связывание всех необходимых компонентов этого модуля в рамках конкретной архитектуры (MVP, MVVM, CleanSwift).
После необходимых приготовлений мы можем реализовать метод performFlow() у AuthorizationCoordinator’а.
Стартовым экраном в рамках данного координатора является EnterView.
В методе performFlow() с помощью фабрики модулей вызывается создание готового модуля для данного координатора, далее реализуется логика обработки замыканий, которые вызывает в тот или иной момент времени наш view controller, затем данный модуль выставляется у роутера корнем в навигационном стеке экранов:
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) } }

Несмотря на кажущуюся местами сложность, данный паттерн идеально подходит для работы с mock-файлами, позволяет полностью изолировать модули друг от друга, а также абстрагирует нас от UIKit, что хорошо подходит для полного покрытия тестами. При этом всем, Coordinator не накладывает строгих требований на архитектуру приложения и является лишь удобным дополнением, структурируя навигацию, зависимости и потоки данных между модулями.
Ссылка на github, где содержится демо на основе Clean архитектуры и удобный Xcode Template для создания необходимых архитектурных слоев.
