
(
Illustration )
Tôt ou tard, chaque équipe commence à penser à introduire ses propres approches architecturales, et de nombreuses copies ont été cassées à ce sujet. Donc, chez
Umbrella IT, nous avons toujours voulu travailler avec des outils flexibles afin que la formation de l'architecture ne soit pas quelque chose de douloureux, et que les problèmes de navigation, de faux fichiers, d'isolement et de test aient cessé d'être quelque chose d'effrayant, quelque chose qui tôt ou tard suspendu au-dessus d'un projet envahi. Heureusement, nous ne parlons pas d'une nouvelle architecture «exclusive» avec une abréviation prétentieuse. Je dois admettre que les architectures actuellement populaires (MVP, MVVM, VIPER, Clean-swift) font face à leurs tâches, et seuls le mauvais choix et la mauvaise utilisation de telle ou telle approche peuvent causer des difficultés. Cependant, dans le cadre de l'architecture adoptée, différents modèles peuvent être utilisés, ce qui permettra d'atteindre ces indicateurs très mythiques: flexibilité, isolement, testabilité, réutilisation.
Bien sûr, les applications sont différentes. Si un projet ne contient que quelques écrans connectés en série, il n'y a pas de besoin particulier d'interactions complexes entre les modules. Il est possible de faire avec les connexions de séquence habituelles, en assaisonnant tout cela avec le bon vieux MVC / MVP. Et bien que le snobisme architectural vienne tôt ou tard à vaincre chaque développeur, la mise en œuvre doit toujours être proportionnée aux objectifs et à la complexité du projet. Et donc, si le projet implique une structure d'écran complexe et divers états (autorisation, mode Invité, hors ligne, rôles pour les utilisateurs, etc.), alors une approche simplifiée de l'architecture jouera certainement un tour: beaucoup de dépendances, un transfert de données non évident et coûteux entre écrans et états, problèmes de navigation et surtout - tout cela n'aura aucune flexibilité et réutilisabilité, les solutions seront étroitement intégrées dans le projet et l'écran A ouvrira toujours l'écran B. Les tentatives de modification entraîneront des refacteurs douloureux ngam pendant lequel il est si facile de faire des erreurs et de casser ce qui fonctionnait. Dans l'exemple ci-dessous, nous allons décrire une manière flexible d'organiser une application qui a deux états: l'utilisateur n'est pas autorisé et doit être dirigé vers l'écran d'autorisation, l'utilisateur est autorisé et un certain écran principal doit être ouvert.
1. Mise en œuvre des principaux protocoles
Nous devons d'abord implémenter la base. Tout commence par les protocoles Coordonnables, Présentables, Routables:
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) } }
Dans cet exemple, showAlert est juste une méthode pratique pour appeler une notification, qui se trouve dans l'extension 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. Créez un coordinateur
De temps en temps, il est nécessaire de changer les écrans d'application, ce qui signifie qu'il sera nécessaire d'implémenter la couche testée sans downcast, ainsi que sans violer les principes SOLID.
Nous procédons à l'implémentation de la couche de coordonnées:

Après le démarrage de l'application, la méthode AppCoordinator doit être appelée, ce qui détermine le flux à démarrer. Par exemple, si l'utilisateur est enregistré, vous devez alors exécuter l'application de flux, et sinon, puis l'autorisation de flux. Dans ce cas, MainCoordinator et AuthorizationCoordinator sont requis. Nous décrirons le coordinateur pour l'autorisation, tous les autres écrans peuvent être créés de manière similaire.
Vous devez d'abord ajouter une sortie au coordinateur afin qu'il puisse avoir une connexion avec un coordinateur supérieur (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 } }

Comme indiqué ci-dessus, nous avons un coordinateur d'autorisation avec un routeur et une usine de modules. Mais qui et quand appelle la méthode start ()?
Ici, nous devons implémenter 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 }
Dans l'exemple, vous pouvez voir qu'AppCoordinator a un routeur, une fabrique de coordinateurs et l'état du point d'entrée pour AppCoordinator, dont le rôle est de déterminer le début du flux pour l'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. Mise en place des coordinateurs d'usine
Chacun des coordinateurs est initialisé avec un routeur et une fabrique de modules. De plus, chacun des coordinateurs doit hériter du coordinateur de base:
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 - une classe qui contient un tableau de coordinateurs enfants et deux méthodes: Supprimer et ajouter une dépendance de coordinateur.
4. Configuration d'AppDelegate
Voyons maintenant à quoi ressemble
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 } }
Dès que la méthode déléguée didFinishLaunchingWithOptions est appelée, la méthode start () de AppCoordinator est appelée, ce qui déterminera la logique supplémentaire de l'application.
5. Création d'un module d'écran
Pour montrer ce qui se passe ensuite, revenons à AuthorizationCoordinator et implémentons la méthode performFlow ().
Tout d'abord, nous devons implémenter l'interface AuthorizationFactoryProtocol dans la classe ModulesFactory:
final class ModulesFactory {} // MARK:- AuthorizationFactoryProtocol extension ModulesFactory: AuthorizationFactoryProtocol { func makeEnterView() -> EnterViewProtocol { let view: EnterViewController = EnterViewController.controllerFromStoryboard(.authorization) EnterAssembly.assembly(with: view) return view
En appelant n'importe quelle méthode dans une fabrique de modules, nous entendons généralement l'initialisation du ViewController à partir du storyboard, puis la liaison de tous les composants nécessaires de ce module au sein d'une architecture spécifique (MVP, MVVM, CleanSwift).
Après les préparatifs nécessaires, nous pouvons implémenter la méthode performFlow () du AuthorizationCoordinator.
L'écran de démarrage de ce coordinateur est EnterView.
Dans la méthode performFlow (), en utilisant la fabrique de modules, la création d'un module prêt à l'emploi pour le coordinateur donné est appelée, puis la logique de traitement des fermetures que notre contrôleur de vue appelle à un moment ou un autre est implémentée, puis ce module est configuré par le routeur comme racine dans la pile de navigation des écrans:
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) } }

Malgré la complexité apparente à certains endroits, ce modèle est idéal pour travailler avec des fichiers fictifs, vous permet d'isoler complètement les modules les uns des autres, et nous résume également d'UIKit, qui est bien adapté pour une couverture complète des tests. Dans le même temps, le coordinateur n'impose pas d'exigences strictes à l'architecture de l'application et n'est qu'un ajout pratique, structurant la navigation, les dépendances et les flux de données entre les modules.
Lien vers github , qui contient une démo basée sur une architecture propre et un modèle Xcode pratique pour créer les couches architecturales nécessaires.