Problemas de patrón de coordinador y qué tiene que ver RouteComposer con él

Continúo la serie de artículos sobre la biblioteca RouteComposer que utilizamos, y hoy quiero hablar sobre el patrón Coordinador. Me pidieron que escribiera este artículo en una discusión de uno de los artículos sobre el patrón: el coordinador aquí en Habré.


El patrón Coordinator, introducido hace poco, está ganando cada vez más popularidad entre los desarrolladores de iOS y, en general, está claro por qué. Porque las herramientas listas para usar que UIKit proporciona no son un desastre universal.


imagen


Ya he planteado la cuestión de la fragmentación de la forma en que compongo la vista de los controladores en la pila, y para evitar la repetición, puede leer sobre esto aquí .


Seamos honestos. En algún momento, Epole se dio cuenta de que al colocar los controladores en el centro de desarrollo de aplicaciones, no ofrecía ninguna forma sensata de crear o transferir datos entre ellos y, habiendo confiado la solución a este problema a los desarrolladores, se completaba automáticamente desde Xcode, y quizás a los desarrolladores de UISearchConnroller en algún momento nos presentó storyboards y segues. Luego, Epolus se dio cuenta de que ella escribió aplicaciones que constaban de 2 pantallas solo para ella, y en la siguiente iteración ofreció la oportunidad de dividir los guiones gráficos en varios componentes, ya que Xcode comenzó a fallar cuando el guión gráfico alcanzó cierto tamaño. Los segmentos han cambiado junto con este concepto, en varias iteraciones que no son muy compatibles entre sí. Su soporte está estrechamente UIViewController clase masiva UIViewController y, al final, obtuvimos lo que obtuvimos. Aquí esta:


 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } } } 

La cantidad de Tycastcasts forzados en este bloque de código es sorprendente, al igual que las constantes de cadena en los propios guiones gráficos, para rastrear qué Xcode no ofrece ningún medio. Y el más mínimo deseo de cambiar algo en el proceso de navegación le permitirá compilar el proyecto sin ningún esfuerzo y se bloqueará con una explosión en tiempo de ejecución sin la más mínima advertencia de Xcode. Aquí hay un WYSIWYG al final que resultó. Lo que ves es lo que obtienes.


Puede discutir durante mucho tiempo sobre los encantos de estas flechas grises en los guiones gráficos que supuestamente muestran a alguien las conexiones entre las pantallas, pero, como lo ha demostrado mi práctica, entrevisté intencionalmente a varios desarrolladores familiares de diferentes compañías, tan pronto como el proyecto creció más allá de 5-6 pantallas, la gente intentó encontré una solución más confiable y finalmente comencé a mantener la estructura de la pila de controladores de vista en mi cabeza. Y si se agregó soporte para iPad y otros modelos de navegación o soporte para empujes, entonces todo fue triste allí.


Desde entonces, se han realizado varios intentos para resolver este problema, algunos de los cuales resultaron en marcos separados, algunos en patrones arquitectónicos separados, ya que la creación de controladores de vista dentro del controlador de vista hizo que este código masivo y torpe fuera aún más.


Volvamos al patrón Coordinador. Por razones obvias, no encontrará su descripción en Wikipedia porque no es un patrón de programación / diseño estándar. Más bien, es una especie de abstracción, que sugiere esconder bajo el capó todo este código "feo" para crear e insertar un nuevo giro de controlador en la pila, guardar enlaces en el contenedor de controladores y enviar datos entre controladores. El artículo más adecuado que describe este proceso llamaría un artículo en raywenderlich.com . Comienza a hacerse popular después de la conferencia NSSpain de 2015, cuando se le informó al público en general. Con más detalle, lo que se dijo se puede encontrar aquí y aquí .


Describiré brevemente en qué consiste antes de continuar.


El patrón Coordinador en todas las interpretaciones se ajusta aproximadamente a esta imagen:



Es decir, el coordinador es un protocolo


 protocol Coordinator { func start() } 

Y se supone que todo el código feo está oculto en la función de start . El coordinador, además, puede tener enlaces con coordinadores secundarios, es decir, tienen cierta capacidad de composición y, por ejemplo, puede reemplazar una implementación por otra. Es decir, suena bastante elegante.


Sin embargo, las locuras comienzan bastante pronto:


  1. Algunas implementaciones proponen convertir el Coordinador de un cierto patrón generador en algo más razonable, observar la pila de controladores y convertirlo en un delegado del contenedor , por ejemplo, UINavigationController , para procesar haciendo clic en el botón Atrás o deslizar hacia atrás y eliminar el coordinador secundario. Por razones naturales, solo un objeto puede ser un delegado, lo que limita el control del contenedor en sí y conduce al hecho de que esta lógica recae en el coordinador o crea la necesidad de delegar esta lógica a alguien más abajo en la lista.
  2. A menudo, la lógica para crear el siguiente controlador depende de la lógica empresarial . Por ejemplo, para ir a la siguiente pantalla, el usuario debe iniciar sesión en el sistema. Claramente, este es un proceso asincrónico, que incluye generar una pantalla intermedia con el formulario de inicio de sesión, el proceso de inicio de sesión en sí puede finalizar con éxito o no. Para evitar transformar el Coordinador en un Coordinador masivo (similar al Controlador de vista masiva), necesitamos descomposición. Es decir, de hecho, necesita crear un Coordinador Coordinador.
  3. Otro problema que enfrentan los coordinadores es que son esencialmente envoltorios para controladores de vista de contenedor como UINavigationController , UITabBarController etc. Y alguien debería proporcionar enlaces a estos controladores . Si con los coordinadores secundarios todo es aún menos claro, entonces con los coordinadores iniciales de la cadena, no todo es tan simple. Además, al cambiar la navegación, por ejemplo para la prueba A / B, la refactorización y la adaptación de dichos coordinadores resultan en un dolor de cabeza por separado. Especialmente si el tipo de contenedor cambia.
  4. Todo esto se vuelve aún más complicado cuando la aplicación comienza a admitir eventos externos que generan controladores de vista. Como notificaciones push o enlaces universales (el usuario hace clic en el enlace de la carta y continúa en la pantalla de la aplicación correspondiente). Aquí surgen otras incertidumbres para las cuales el patrón Coordinador no tiene una respuesta exacta. Debe saber exactamente en qué pantalla se encuentra actualmente el usuario para mostrarle la siguiente pantalla solicitada por un evento externo.
    El ejemplo más simple es una aplicación de chat que consta de 3 pantallas: una lista de chat, el chat en sí que se introduce en la navegación del controlador de la lista de chat y la pantalla de configuración que se muestra modalmente. El usuario puede estar en una de estas pantallas cuando recibe una notificación push y toca en ella. Y aquí comienza la incertidumbre, si él está en la lista de chat, debe iniciar un chat con este usuario específico, si ya está en el chat, entonces debe cambiarlo, y si ya está en el chat con este usuario, no haga nada y actualice, si el usuario está activado pantalla de configuración: aparentemente necesita cerrar y seguir los pasos anteriores. ¿O tal vez no cerrar y simplemente mostrar el chat modalmente en la configuración? ¿Y si la configuración está en otra pestaña, y no modal? Estos if/else comienzan se extienden sobre los coordinadores o van a otro Mega-Coordinador en forma de un espagueti. Además, se trata de iteraciones activas en la pila de vistas de los controladores y un intento de determinar dónde está el usuario en este momento, o un intento de construir algún tipo de aplicación que monitoree su estado, pero esta no es una tarea fácil, solo en función de la naturaleza de la pila de controladores de vista.
  5. Y la guinda del pastel son las fallas de UIKit . Un ejemplo trivial: un UITabBarController con un UINavigationController en la segunda pestaña con algún otro UIViewController . El usuario en la primera pestaña causa un determinado evento que requiere cambiar la pestaña y UINavigationController otro controlador de vista a su UINavigationController . Todo esto debe hacerse exactamente en esa secuencia. Si el usuario nunca abrió una segunda pestaña antes de esto y no se llamó a viewDidLoad en el viewDidLoad el método push no funcionará, dejando solo un mensaje indistinto en la consola. Es decir, los coordinadores no pueden simplemente convertirse en oyentes de eventos en este ejemplo, sino que deben trabajar en una secuencia determinada. Entonces deben tener conocimiento el uno del otro. Y esto ya contradice la primera declaración del patrón Coordinador, que los coordinadores no saben nada sobre los coordinadores generadores y están conectados solo con los hijos. Y también limita su intercambiabilidad.

Esta lista puede continuar, pero en general está claro que el patrón Coordinador es una solución bastante limitada y poco escalable. Si lo miras sin gafas de color rosa, entonces es una forma de descomponer parte de la lógica, que generalmente se escribe dentro de los UIViewController masivos, en otra clase. Todos los intentos de hacerlo más que una fábrica generativa e introducir otra lógica allí no terminan bien.


Vale la pena señalar que hay bibliotecas basadas en este patrón, que, de una forma u otra, permiten mitigar parcialmente las desventajas anteriores. Mencionaría XCoordinator y RxFlow .


Que hemos hecho


Después de haber jugado en el proyecto que obtuvimos de otro equipo de apoyo y desarrollo, con los coordinadores y su enrutador simplificado de "bisabuela" en el enfoque arquitectónico VIPER , volvimos al enfoque que funcionó bien en el gran proyecto anterior de nuestra empresa. Este enfoque no tiene nombre. Se encuentra en la superficie. Cuando teníamos tiempo libre, se compiló en una biblioteca RouteComposer separada que reemplazó por completo a los coordinadores y demostró ser más flexible.


¿Cuál es este enfoque? En eso, para confiar en la pila (árbol), giro los controladores tal como están. Para no crear entidades innecesarias que necesitan ser monitoreadas. No guarde ni rastree las condiciones.


Echemos un vistazo más de cerca a las entidades UIKit e intentemos averiguar qué tenemos en el resultado final y con qué podemos trabajar:


  1. La pila del controlador es un árbol. Hay un controlador de vista raíz que tiene controladores de vista secundarios. Los controladores de vista presentados modalmente son un caso especial de los controladores de vista secundarios, ya que también tienen un enlace con el controlador de vista generado. Todo está disponible fuera de la caja.
  2. Necesito crear entidades de controladores. Todos tienen constructores diferentes; se pueden crear usando archivos Xib o Storyboards. Tienen diferentes parámetros de entrada. Pero están unidos porque necesitan ser creados. Entonces, aquí podemos usar el patrón Factory , que sabe cómo crear el controlador de vista deseado. Cada fábrica es fácil de cubrir con exhaustivas pruebas unitarias y es independiente de otras.
  3. Dividimos los controladores de vista en 2 clases: 1. Simplemente vea los controladores, 2. Controladores de vista de contenedor (Controlador de vista de contenedor) . Los controladores de vista de contenedor difieren de los ordinarios en que pueden contener controladores de vista secundarios, también contenedores o simples. Dichos controladores de vista están disponibles de UINavigationController : UINavigationController , UINavigationController , etc., pero también pueden ser creados por el usuario. Si lo ignoramos, podemos encontrar las siguientes propiedades en todos los contenedores: 1. Tienen una lista de todos los controladores que contienen. 2. Uno o más controladores están actualmente visibles. 3. Se les puede pedir que hagan visible uno de estos controladores. Esto es todo lo que pueden hacer los controladores UIKit . Simplemente tienen diferentes métodos para esto. Pero solo hay 3 tareas.
  4. Para incrustar un controlador de vista creado de fábrica, el método de vista principal del controlador es UINavigationController.pushViewController(...) , UITabBarController.selectedViewController = ... , UIViewController.present(...) y así sucesivamente. Puede observar que siempre se requieren 2 controladores de vista, uno que ya está en la pila y otro que debe incrustarse en la pila. Envuelva esto en un contenedor y llámelo Acción (Acción) . Cada acción es fácil de cubrir con pruebas unitarias completas y cada una es independiente de las demás.
  5. De lo anterior, resulta que usando entidades preparadas, puede construir la cadena de configuración Fábrica -> Acción -> Fábrica -> Acción -> Fábrica y, después de completarla, puede construir un árbol de vista de controladores de cualquier complejidad. Solo necesita especificar el punto de entrada. Estos puntos de entrada suelen ser el rootViewController propiedad de UIWindow o el controlador de vista actual, que es la rama más extrema del árbol. Es decir, dicha configuración se escribe correctamente como: Inicio de ViewController -> Acción -> Fábrica -> ... -> Fábrica .
  6. Además de la configuración, necesitará alguna entidad que sepa cómo iniciar y construir la configuración proporcionada. Lo llamaremos enrutador . No tiene un estado, no contiene ningún enlace. Tiene un método al que se pasa la configuración y realiza secuencialmente los pasos de configuración.
  7. Agregue responsabilidad al enrutador agregando clases de interceptores a la cadena de configuración. Los interceptores son posibles de 3 tipos: 1. Lanzado antes de comenzar la navegación. Eliminamos las tareas de autenticación de usuarios en el sistema y otras tareas asincrónicas en ellos. 2. Ejecute en el momento de la creación del controlador de vista para establecer los valores. 3. Realizado después de la navegación y realizando diversas tareas analíticas. Cada entidad se cubre fácilmente mediante pruebas unitarias y no sabe cómo se utilizará en la configuración. Ella tiene una sola responsabilidad y la cumple. Es decir, la configuración para la navegación compleja puede verse como [Tarea previa a la navegación ...] -> Iniciar ViewController -> Acción -> (Factory + [ContextTask ...]) -> ... -> (Factory + [ContextTask ...]) -> [Post NavigationTask ...] Es decir, todas las tareas serán realizadas por el enrutador secuencialmente, realizando a su vez entidades atómicas pequeñas y fácilmente legibles.
  8. La última tarea que la configuración no puede resolver permanece: este es el estado de la aplicación en este momento. ¿Qué sucede si necesitamos construir no toda la cadena de configuración, sino solo una parte, porque el usuario la pasó parcialmente? Esta pregunta siempre puede ser respondida inequívocamente por los controladores del árbol de visión. Porque si parte de la cadena ya está construida, ya está en el árbol. Esto significa que si cada fábrica de la cadena puede responder a la pregunta de si está construida o no, entonces el enrutador podrá comprender qué parte de la cadena debe completarse. Por supuesto, esta no es la tarea de la fábrica, por lo que se introduce otra entidad atómica: el Finder, y cualquier configuración se ve así: [Tarea previa a la navegación ...] -> Iniciar ViewController -> Acción -> (Finder / Factory + [ContextTask ...]) -> ... -> (Finder / Factory + [ContextTask ...]) -> [Publicar tarea de navegación ...] . Si el enrutador comienza a leerlo desde el final, entonces uno de los buscadores le dirá que ya está construido, y el enrutador a partir de este punto comenzará a construir la cadena de nuevo. Si ninguno de ellos se encuentra en el árbol, debe construir toda la cadena desde el controlador inicial.
    imagen
  9. La configuración debe estar fuertemente tipada. Por lo tanto, cada entidad trabaja con un solo tipo de vista de controlador; un tipo de datos y configuración se basa completamente en la capacidad de swift para trabajar con los tipos asociados . Queremos confiar en el compilador, no en el tiempo de ejecución. Un desarrollador puede debilitar la escritura intencionalmente, pero no al revés.

Un ejemplo de tal configuración:


 let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719 is fixed .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralActions.presentModally()) .from(GeneralStep.current()) .assemble() 

Los elementos descritos anteriormente cubren toda la biblioteca y describen el enfoque. Todo lo que nos queda es proporcionar las configuraciones de cadena que el enrutador ejecutará cuando el usuario haga clic en un botón o se produzca un evento externo. Si se trata de diferentes tipos de dispositivos, por ejemplo, iPhone o iPad, proporcionaremos diferentes configuraciones de transición utilizando polimorfismo. Si tenemos pruebas A / B, lo mismo. No necesitamos pensar en el estado de la aplicación al momento de iniciar la navegación, debemos asegurarnos de que la configuración esté escrita correctamente inicialmente, y estamos seguros de que el enrutador la construirá de alguna manera.


El enfoque descrito es más complicado que una cierta abstracción o patrón, pero aún no hemos enfrentado el problema donde no sería suficiente. Por supuesto, RouteComposer requiere un poco de estudio y comprensión de cómo funciona. Sin embargo, esto es mucho más fácil que aprender los conceptos básicos de AutoLayout o RunLoop. No más matemáticas.


La biblioteca, así como la implementación del enrutador que se le proporciona, no utiliza ningún truco objetivo con el tiempo de ejecución y sigue completamente todos los conceptos de Cocoa Touch, solo ayuda a dividir el proceso de composición en pasos y ejecutarlos en la secuencia dada. La biblioteca se prueba con las versiones de iOS 9 a 12.


Se pueden encontrar más detalles en artículos anteriores:
Composición de UIViewControllers y navegación entre ellos (y no solo) / revista geek
Ejemplos de configuración de UIViewControllers usando RouteComposer / geek magazine


Gracias por su atencion Estaré encantado de responder preguntas en los comentarios.

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


All Articles