Cuando aprendí a desarrollar aplicaciones para iOS uno de los tópicos que se incluyen es cómo manejar la navegación dentro de la aplicación o cómo ir de una pantalla a otra.
Para enfrentar este problema tenemos un mecanismo provisto por Apple y Xcode: Storyboards y Segues
Al principio me parecía algo tan natural, después de todo, usar las herramientas que te provee Apple es la mayoría de las veces una apuesta segura.
Esto funciona para aplicaciones pequeñas, pero una vez que tu aplicación crece te enfrentas a distintos problemas:
- La navegación es estática y usando storyboards se suele salir de control fácilmente.
- Los View Controllers terminan con código que corresponde a la navegación (demasiada responsabilidad)
- Alto acoplamiento entre ViewControllers (porqué el ViewController A tiene que conocer que debe mostrar el ViewController B y es más, por qué debe saber cómo crear o inyectar dependencias a este ViewController B)
- Código repetitivo
- Problemas de reusabilidad
- Uso de una misma pantalla en distintos contextos
- A/B testing
- Push Notifications
Coordinadores
La idea de un Coordinador es simple.
Es probable que en tu aplicación tengas varios flujos agrupados de acuerdo al tu lógica de negocio (Onboarding, Registro de Usuario, Settings, Perfil de Usuario, Carrito de compras, Nuevo Pedido, etc)
Ahora cada uno de estos flujos sería manejado por un Coordinador.
El trabajo del Coordinador es saber qué ViewController mostrar.
Supongamos que tengo un Onboarding, al finalizar su trabajo este le comunica a su Coordinador que ha terminado o que el usuario ha accionado una acción que implica mostrar otro ViewController.
El Coordinador sabe hacia dónde tiene que ir y cómo hacerlo.
Esto supone aligerar nuestros ViewControllers, ya que les estás quitando la responsabilidad de manejar la navegación.
Ventajas:
- Reusabilidad de los ViewControllers
- Permitir presentar un Viewcontroller en diferentes contextos
- Fácil implementación de Inyección de Dependencias
- Remueve código relacionado a la navegación de los View Controllers
¿Cómo Implementarlo?
Si bien existen varios Frameworks: XCoordinator, Coordinator, RxFlow
Pero podrías empezar con algo más simple:
Child Coordinators:
Si tienes una aplicación grande puedes dividir un coordinador en Coordinadores hijos, cada uno responsable de manejar cierto flujo de la aplicación.
Estos a su vez pueden tener otros Coordinadores hijos y así sucesivamente.
Cuando un Coordinador hijo termina le informa a su padre que ha terminado.
Steps:
Este es un concepto que lo ocupé de aquí:
Se podría definir un Step como un camino o flujo de navegación dentro de un Coordinador.
Usando Steps dentro de nuestro Coordinador:
Ejemplo:
Supongamos que tenemos nuestro ContactsCoordinator, mediante esta enumeración definimos las posibles acciones que puede manejar este Coordinador:
La gran ventaja de usar una Enumeración para definir los Steps es que los posibles flujos de navegación de un Coordinador son explícitos que nos permiten tener rápidamente una visión general del Coordinador
Para instanciar el Coordinador con el Step por defecto lo haría de la siguiente manera:
¿Qué pasa si deseo instanciar el Coordinator desde una notificación? O simplemente no deseo router hacia una opción dentro del menú.
Manejando las dependencias
Si habrás notado, la creación de los ViewControllers es sencilla, no tiene ninguna dependencia, pero en una app real indiferente del patrón de diseño (MVC, MVVM, VIPER, Clean Swift, etc) que estemos usando nuestros ViewControllers necesitan dependencias.
Tal vez necesite inyectar un ViewModel?, un Service?, un UseCase?, un Repository? Un Interactor?
Como sentido común sería incluir la lógica de creación de nuestros VC dentro del Coordinador, pero a medida esto implicaría que el Coordinador separa acerca del manejo de estas dependencias y lo que conlleva (Alcance de las dependencias, Ciclo de vida, etc)
A nuestro Coordinador no le debería importar cómo se crean los ViewControllers, internamente puedo estar usando Storyboards, XIB files, por código, etc.
Ni tampoco las dependencias que se requieren para crear estos componentes
Para enfrentar este problema creamos un objeto cuyo trabajo será crear estos ViewControllers.
Ahora dentro de nuestro Coordinator podemos usar este container para crear nuestros View Controllers:
Comunicación desde ViewControllers hacia Coordinadores
Independientemente del patrón que utilices, necesitarás una forma de comunicarte hacia el coordinador.
Para cumplir con esto tenemos varias formas, las más comunes son usando Protocolos o usando Closures.
La más sencilla es usando closures, x ejemplo:
Importante recordar que cuando usamos closures, debemos usar weak self para prevenir leaks de memoria.
Comunicación entre Coordinadores
Otro punto importante que controlar es la forma en cómo finalizan nuestros Coordinadores, se pueden dar en 2 situaciones:
- La vista principal que maneja el Coordinador desaparece de la pila de navegación producto de una acción del usuario.
- La vista desaparece porque el usuario pulsó en el botón "Atrás" en nuestra navegación.
El primer caso es fácil de manejar, al igual que el punto anterior, puedo inyectar al ViewController con un closure en el cual puedo controlar el momento cuando el usuario realiza determinada acción.
El segundo requiere un poco más de trabajo, ya que usualmente delegamos el comportamiento de la navegación a lo que está construido por defecto en UIKit.
En la mayoría de los casos no tienes que preocuparte cuando el usuario pulsó el botón atrás para liberar recursos.
Controlando la navegación
Al usar Child Coordinators debemos controlar que estos terminen su ciclo de vida correctamente removiéndolos del arreglo del Coordinador padre.
Una posible solución para esto sería escuchar los eventos que envía UINavigationControllerDelegate y estar escuchando en el Coordinador padre cuando una vista es removida de la pila de navegación.
Otra posible solución es reescribir el botón Atrás con un custom Button e informar Coordinador que está finalizando la vista principal.
Particularmente prefiero usar una especie de "hack" que consiste en agregar una última llamada en la vista Principal de un Child Coordinator antes de que termine de desinicializarse.
Le podría inyectar un Closure y llamar correctamente al método Stop del Coordinator.
Conclusión:
Listo eso concluye gran parte de la forma cómo usamos el patrón Coordinador en Sosafe.
Esto nos ha facilitado controlar mejor la navegación dentro de la aplicación, usar nuestros ViewControllers en distintos contextos y hacerlos más livianos.
Espero te sirva esta información y la puedas utilizar en tu próximo proyecto.
He tratado de reducir mucho del código para poder generar los snippets.
En este proyecto he incluido varios de los puntos citados en este post:
https://github.com/rcaos/DemoCoordinators
Autor: Jeans Ruiz
Fuentes:
https://www.raywenderlich.com/158-coordinator-tutorial-for-ios-getting-started
https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps
https://www.hackingwithswift.com/articles/175/advanced-coordinator-pattern-tutorial-ios