
(
Abbildung )
Jedes Team beginnt früher oder später darüber nachzudenken, seine eigenen architektonischen Ansätze einzuführen, und viele Kopien wurden darüber gebrochen. Deshalb wollten
wir bei
Umbrella IT immer mit flexiblen Tools arbeiten, damit die Bildung von Architektur nicht schmerzhaft war und die Probleme der Navigation, der Mock-Dateien, der Isolation und des Testens früher oder später nicht mehr beängstigend waren über einem überwucherten Projekt hängen. Glücklicherweise handelt es sich nicht um eine neue „exklusive“ Architektur mit einer prätentiösen Abkürzung. Ich muss zugeben, dass die derzeit gängigen Architekturen (MVP, MVVM, VIPER, Clean-swift) ihre Aufgaben bewältigen und nur die falsche Wahl und die falsche Verwendung dieses oder jenes Ansatzes zu Schwierigkeiten führen kann. Im Rahmen der angenommenen Architektur können jedoch verschiedene Muster verwendet werden, mit denen diese fast mythischen Indikatoren erreicht werden können: Flexibilität, Isolation, Testbarkeit, Wiederverwendung.
Natürlich sind die Anwendungen unterschiedlich. Wenn ein Projekt nur wenige Bildschirme enthält, die in Reihe geschaltet sind, sind keine komplexen Interaktionen zwischen Modulen erforderlich. Es ist möglich, mit den üblichen Segue-Verbindungen zu arbeiten und all dies mit dem guten alten MVC / MVP zu würzen. Und obwohl der architektonische Snobismus früher oder später jeden Entwickler besiegt, sollte die Implementierung dennoch den Zielen und der Komplexität des Projekts entsprechen. Wenn das Projekt eine komplexe Bildschirmstruktur und verschiedene Zustände (Autorisierung, Gastmodus, Offline, Rollen für Benutzer usw.) umfasst, spielt ein vereinfachter Architekturansatz sicherlich eine Rolle: viele Abhängigkeiten, eine nicht offensichtliche und teure Datenübertragung zwischen Bildschirme und Zustände, Probleme mit der Navigation und vor allem - all dies wird keine Flexibilität und Wiederverwendbarkeit haben, Lösungen werden eng in das Projekt eingebunden und Bildschirm A öffnet immer Bildschirm B. Versuche, Änderungen vorzunehmen, führen zu schmerzhaften Refaktoren ngam, in dem es so einfach ist, Fehler zu machen und das zu brechen, was früher funktioniert hat. Im folgenden Beispiel wird eine flexible Methode zum Organisieren einer Anwendung mit zwei Status beschrieben: Der Benutzer ist nicht autorisiert und sollte zum Autorisierungsbildschirm geleitet werden, der Benutzer ist autorisiert und ein bestimmter Hauptbildschirm sollte geöffnet werden.
1. Implementierung der Hauptprotokolle
Zuerst müssen wir die Basis implementieren. Alles beginnt mit den Protokollen 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) } }
In diesem Beispiel ist showAlert nur eine bequeme Methode zum Aufrufen einer Benachrichtigung, die sich in der UIViewController-Erweiterung befindet. 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. Erstellen Sie einen Koordinator
Von Zeit zu Zeit müssen die Anwendungsbildschirme geändert werden. Dies bedeutet, dass die getestete Schicht ohne Downcast sowie ohne Verletzung der SOLID-Prinzipien implementiert werden muss.
Wir fahren mit der Implementierung der Koordinatenschicht fort:

Nach dem Starten der Anwendung sollte die AppCoordinator-Methode aufgerufen werden, die festlegt, welcher Flow gestartet werden soll. Wenn der Benutzer beispielsweise registriert ist, sollten Sie die Flow-Anwendung ausführen, und wenn nicht, die Flow-Autorisierung. In diesem Fall sind MainCoordinator und AuthorizationCoordinator erforderlich. Wir werden den Koordinator für die Autorisierung beschreiben, alle anderen Bildschirme können auf ähnliche Weise erstellt werden.
Zuerst müssen Sie dem Koordinator eine Ausgabe hinzufügen, damit er eine Verbindung mit einem höheren Koordinator (AppCoordinator) herstellen kann:
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 } }

Wie oben gezeigt, haben wir einen Autorisierungskoordinator mit einem Router und einer Modulfabrik. Aber wer und wann ruft die start () -Methode auf?
Hier müssen wir AppCoordinator implementieren.
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 }
Aus dem Beispiel können Sie ersehen, dass AppCoordinator über einen Router, eine Koordinatorfactory und den Status des Einstiegspunkts für AppCoordinator verfügt, dessen Aufgabe es ist, den Start des Ablaufs für die Anwendung zu bestimmen.
final class CoordinatorFactory { fileprivate let modulesFactory = ModulesFactory() } extension CoordinatorFactory: CoordinatorFactoryProtocol { func makeAuthorizationCoordinator(with router: Routable) -> Coordinatable & AuthorizationCoordinatorOutput { return AuthorizationCoordinator(router: router, factory: modulesFactory) } }
3. Implementierung der Fabrikkoordinatoren
Jeder der Koordinatoren wird mit einem Router und einer Modulfabrik initialisiert. Darüber hinaus muss jeder der Koordinatoren vom Basiskoordinator erben:
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 - Eine Klasse, die ein Array von untergeordneten Koordinatoren und zwei Methoden enthält: Löschen und Hinzufügen der Koordinatorabhängigkeit.
4. AppDelegate konfigurieren
Nun wollen wir sehen, wie
UIApplicationMain aussieht
: @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 } }
Sobald die Delegate-Methode didFinishLaunchingWithOptions aufgerufen wird, wird die start () -Methode des AppCoordinator aufgerufen, die die weitere Logik der Anwendung bestimmt.
5. Erstellen eines Bildschirmmoduls
Um zu demonstrieren, was als nächstes passiert, kehren wir zum AuthorizationCoordinator zurück und implementieren die performFlow () -Methode.
Zuerst müssen wir die AuthorizationFactoryProtocol-Schnittstelle in der ModulesFactory-Klasse implementieren:
final class ModulesFactory {} // MARK:- AuthorizationFactoryProtocol extension ModulesFactory: AuthorizationFactoryProtocol { func makeEnterView() -> EnterViewProtocol { let view: EnterViewController = EnterViewController.controllerFromStoryboard(.authorization) EnterAssembly.assembly(with: view) return view
Mit dem Aufrufen einer beliebigen Methode in einer Modulfactory meinen wir in der Regel das Initialisieren des ViewControllers über das Storyboard und das Verknüpfen aller erforderlichen Komponenten dieses Moduls innerhalb einer bestimmten Architektur (MVP, MVVM, CleanSwift).
Nach den erforderlichen Vorbereitungen können wir die performFlow () -Methode des AuthorizationCoordinator implementieren.
Der Startbildschirm in diesem Koordinator ist EnterView.
Bei der performFlow () -Methode wird unter Verwendung der Modulfactory die Erstellung eines vorgefertigten Moduls für den angegebenen Koordinator aufgerufen, dann wird die Logik zum Verarbeiten der Schließungen implementiert, die unser View Controller zu der einen oder anderen Zeit aufruft, und dieses Modul wird vom Router als Stamm im Navigationsstapel der Bildschirme eingerichtet:
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) } }

Trotz der an einigen Stellen scheinbaren Komplexität ist dieses Muster ideal für die Arbeit mit Mock-Dateien, ermöglicht es Ihnen, die Module vollständig voneinander zu isolieren, und abstrahiert uns auch von UIKit, das für eine vollständige Testabdeckung gut geeignet ist. Gleichzeitig stellt Coordinator keine strengen Anforderungen an die Architektur der Anwendung und ist nur eine bequeme Ergänzung, die Navigation, Abhängigkeiten und Datenflüsse zwischen Modulen strukturiert.
Link zu Github , das eine Demo auf Basis der Clean-Architektur und eine praktische Xcode-Vorlage zum Erstellen der erforderlichen Architekturebenen enthält.