Crear una arquitectura: trabajar con el patrón Coordinador de iOS


( Ilustración )

Cada equipo, tarde o temprano, comienza a pensar en presentar sus propios enfoques arquitectónicos, y se rompieron muchas copias al respecto. Entonces, en Umbrella IT siempre quisimos trabajar con herramientas flexibles para que la formación de la arquitectura no fuera algo doloroso, y los problemas de navegación, simulacros, aislamiento y pruebas dejaron de ser algo aterrador, algo que tarde o temprano colgando sobre un proyecto descuidado. Afortunadamente, no estamos hablando de una nueva arquitectura "exclusiva" con una abreviatura pretenciosa. Debo admitir que las arquitecturas actualmente populares (MVP, MVVM, VIPER, Clean-swift) están haciendo frente a sus tareas, y solo la elección incorrecta y el uso incorrecto de este o aquel enfoque pueden causar dificultades. Sin embargo, dentro del marco de la arquitectura adoptada, se pueden usar varios patrones, lo que permitirá alcanzar esos indicadores casi míticos: flexibilidad, aislamiento, comprobabilidad, reutilización.

Por supuesto, las aplicaciones son diferentes. Si un proyecto contiene solo unas pocas pantallas que están conectadas en serie, entonces no hay una necesidad particular de interacciones complejas entre módulos. Es posible hacer con las conexiones segue habituales, condimentando todo esto con el viejo MVC / MVP. Y aunque el esnobismo arquitectónico tarde o temprano derrota a todos los desarrolladores, la implementación debe ser acorde con los objetivos y la complejidad del proyecto. Y así, si el proyecto involucra una estructura de pantalla compleja y varios estados (autorización, modo Invitado, fuera de línea, roles para usuarios, etc.), entonces un enfoque simplificado de la arquitectura sin duda jugará un truco: muchas dependencias, una transferencia de datos costosa y obvia entre pantallas y estados, problemas con la navegación, y lo más importante: todo esto no tendrá flexibilidad y reutilización, las soluciones se fusionarán estrechamente con el proyecto, y la pantalla A siempre abrirá la pantalla B. Los intentos de hacer cambios darán lugar a refactores dolorosos ngam durante el cual es tan fácil cometer errores y romper lo que solía funcionar. En el siguiente ejemplo, describiremos una forma flexible de organizar una aplicación que tiene dos estados: el usuario no está autorizado y debe ser dirigido a la pantalla de autorización, el usuario está autorizado y se debe abrir una pantalla principal determinada.

1. Implementación de los principales protocolos.


Primero necesitamos implementar la base. Todo comienza con los protocolos 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) } } 

En este ejemplo, showAlert es solo un método conveniente para llamar a una notificación, que se encuentra en la extensión 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. Crear un coordinador


De vez en cuando, es necesario cambiar las pantallas de la aplicación, lo que significa que será necesario implementar la capa probada sin downcast, así como sin violar los principios de SOLID.

Procedemos a la implementación de la capa de coordenadas:



Después de iniciar la aplicación, se debe llamar al método AppCoordinator, que determina qué flujo se debe iniciar. Por ejemplo, si el usuario está registrado, entonces debe ejecutar la aplicación de flujo, y si no, la autorización de flujo. En este caso, se requieren MainCoordinator y AuthorizationCoordinator. Describiremos al coordinador para la autorización, todas las demás pantallas se pueden crear de manera similar.

Primero debe agregar salida al coordinador para que pueda tener una conexión con un coordinador superior (Coordinador de aplicaciones):

 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 } } 



Como se muestra arriba, tenemos un coordinador de autorización con un enrutador y una fábrica de módulos. ¿Pero quién y cuándo llama al método start ()?
Aquí necesitamos implementar 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 } 

En el ejemplo, puede ver que AppCoordinator tiene un enrutador, una fábrica de coordinadores y el estado del punto de entrada para AppCoordinator, cuya función es determinar el inicio del flujo de la aplicación.

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

3. Implementación de los coordinadores de fábrica.


Cada uno de los coordinadores se inicializa con un enrutador y una fábrica de módulos. Además, cada uno de los coordinadores debe heredar del coordinador 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: una clase que contiene una matriz de coordinadores secundarios y dos métodos: Eliminar y Agregar dependencia del coordinador.

4. Configuración de AppDelegate


Ahora veamos cómo se ve 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()) } } 

Tan pronto como se llama al método delegado didFinishLaunchingWithOptions, se llama al método start () del AppCoordinator, que determinará la lógica adicional de la aplicación.

5. Crear un módulo de pantalla


Para demostrar lo que sucede después, volvamos al Coordinador de autorización e implementemos el método performFlow ().

Primero, necesitamos implementar la interfaz AuthorizationFactoryProtocol en la clase ModulesFactory:

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

Al llamar a cualquier método en una fábrica de módulos, por regla general, nos referimos a inicializar ViewController desde el guión gráfico y luego vincular todos los componentes necesarios de este módulo dentro de una arquitectura específica (MVP, MVVM, CleanSwift).

Después de los preparativos necesarios, podemos implementar el método performFlow () del AuthorizationCoordinator.
La pantalla de inicio dentro de este coordinador es EnterView.
En el método performFlow (), usando la fábrica de módulos, se llama la creación de un módulo listo para el coordinador dado, luego se implementa la lógica de procesamiento de los cierres que nuestro controlador de vista llama en un momento u otro, luego el router configura este módulo como la raíz en la pila de pantallas de navegación:

 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) } } 




A pesar de la aparente complejidad en algunos lugares, este patrón es ideal para trabajar con archivos simulados, le permite aislar completamente los módulos entre sí y también nos abstrae de UIKit, que es muy adecuado para una cobertura de prueba completa. Al mismo tiempo, el Coordinador no impone requisitos estrictos en la arquitectura de la aplicación y es solo una adición conveniente, estructurando la navegación, las dependencias y los flujos de datos entre los módulos.

Enlace a github , que contiene una demostración basada en la arquitectura Clean y una conveniente plantilla Xcode para crear las capas arquitectónicas necesarias.

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


All Articles