Aprende a usar los Coordinators en la Arquitectura Model View Controller
Aprende a usar los Coordinators en la Arquitectura Model View Controller

Aprende a usar la Arquitectura Model-View-Controller con Coordinators en Swift

Al usar arquitecturas podemos aplicar diferentes patrones. En este ejemplo vamos a usar el Patrón Coordinator dentro de la arquitectura Model View Controller en Swift. Este patrón nos permite encapsular la lógica de navegación dentro de una clase que solo tiene esta responsabilidad

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
🤩 ¡Sígueme en Twitter!
▶️ ¡Suscríbete al canal!
📙 COMPRAR EL LIBRO DE SWIFT ⭐️
Patrón Coordinator en la Arquitectura Model View Controller

Hoy en SwiftBeta vamos a aprender a añadir Coordinators dentro de la Arquitectura Model-View-Controller. Lo que vimos en el anterior video fue la arquitectura Model-View-Controller, donde creábamos una app desde cero. Esta app realizaba una petición HTTP a la API de rick and morty, y también realizaba una navegación al pulsar uno de los personajes en la lista. Esta nueva vista mostraba información de detalle del personaje seleccionado. Todo esto lo hicimos siguiendo la arquitectura Model-View-Controller, una arquitectura muy usada dentro del mundo iOS.

Al finalizar el anterior video, navegábamos de un ViewController a otro, y esta lógica de navegación la teníamos dentro del ViewController inicial (Esta que os estoy mostrando ahora mismo). Sabíamos exactamente qué celda del UITableView se había pulsado, y en base a esa información recuperábamos el modelo y se lo pasábamos al ViewController que se iba a mostrar. Pues bien, poara poder desacoplar esta lógica vamos a usar un patrón muy usado en aplicaciones móviles llamado Coordinators. De esta manera crearemos un componente nuevo que sabrá como manejar la navegación dentro de nuestra app, quitándole esta responsabilidad al ViewController.

Esto tiene muchas ventajas, desde el desacople de lógica, poder reutilizar navegaciones en otras partes de tu app, crear multiples navegaciones push o modales, etc.

Coordinators

Vamos a continuar con nuestro proyecto, el que vimos en el anterior video.
Los coordinators son muy sencillos de implementar, y hoy vamos a ver un ejemplo muy básico pero potente. Lo que vamos hacer es crear una carpeta llamada Coordinators, y dentro de ella vamos a crear un fichero nuevo que tendrá el siguiente protocolo:

import Foundation
import UIKit

protocol Coordinator {
    var viewController: UIViewController? { get }
    var navigationController: UINavigationController? { get }

    func start()
}

extension Coordinator {
    var viewController: UIViewController? { nil }
    var navigationController: UINavigationController? { nil }
}
Protocolo Coordinator que conformaremos en todos nuestro Coordinators

Lo único que he hecho ha sido crear un protocolo llamado Coordinator. Este protocolo tiene dos variables opcionales llamadas viewController y navigationController. Estas variables nos servirán para hacer un present en el caso de querer presentar de forma modal un ViewController. O un push en el caso de presentar un ViewController cuando usamos un NavigationController.
También, hay un método llamado start que es el que lanzará la acción de navegar. Todo esto lo vamos a implementar ahora en un MainCoordinator.
Para finalizar, he añadido un valor por defecto de nil a las variables del protocolo Coordinator.

Lo que vamos a crear ahora es un MainCoordinator. Es muy interesante por que lo vamos a llamar cuando se ejecute nuestra app.

import Foundation
import UIKit

class MainCoordinator: Coordinator {
    var navigationController: UINavigationController?
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    // code...
}
Primer coordinator de nuestra app llamado MainCoordinator

Al crear nuestro MainCoordinator, hacemos que conforme nuestro protocolo Coordinator. En este caso vamos a inicializarlo con un NavigationController que le inyectaremos cuando lo inicialicemos desde el SceneDelegate en los siguientes minutos.
Para acabar de conformar el protocolo Coordinator, necesitamos implementar el método start(), pues vamos a ello:

func start() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let listOfCharactersViewController = storyboard.instantiateViewController(withIdentifier: "CharactersListViewController")
    navigationController?.pushViewController(listOfCharactersViewController, animated: true)
}
Creamos la implementación del método start que será el encargado de realizar la navegación

Al cargar nuestro ViewController desde el Storyboard (Como vimos en el video sobre Storyboards y XIBs), necesitamos instanciar CharactersListViewController de esa manera.

Una vez tenemos esta parte listada, tenemos que ir a nuestro punto de entrada de la app:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var mainCoordinator: MainCoordinator?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let scene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: scene)
        let navigationController = UINavigationController()
        mainCoordinator = MainCoordinator(navigationController: navigationController)
        mainCoordinator?.start()
        
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
    }
}
Añadimos nuestro MainCoordinator en el punto de entrada de nuestra app

Ahora ya estamos listos para utilizar nuestro MainCoordinator, si compilamos vamos a ver qué ocurre. Obtenemos un crash, eso es porque nos hemos dejado un paso, hay que añadir el identificador del view controller en el Storyboard, de esta manera podemos instanciar correctamente el ViewController CharactersListViewController (esto también lo vimos en el video de Storyboards)

Añadimos un Storyboard ID para que funcione la instancia de nuestro ViewController
Añadimos un Storyboard ID para que funcione la instancia de nuestro ViewController

Una vez añadido el Storyboard ID, vamos a volver a compilar.

Perfecto, hemos creado nuestro primer Coordinator. Ahora vamos a añadir otro ViewController, vamos a crear una nueva clase llamada CharacterDetailPushCoordinator

import Foundation
import UIKit

final class CharacterDetailPushCoordinator: Coordinator {
    let characterModel: CharacterModel
    var navigationController: UINavigationController?
    
    init(characterModel: CharacterModel, navigationController: UINavigationController?) {
        self.characterModel = characterModel
        self.navigationController = navigationController
    }
    
    func start() {
        let characterDetailViewController = CharacterDetailViewController(characterDetailModel: characterModel)
        self.navigationController?.pushViewController(characterDetailViewController, animated: true)
    }
}
Creamos un Coordinator para hacer push desde un UINavigationController

Esta clase tiene lo necesario para realizar la navegación que estábamos haciendo para poder navegar al CharacterDetail. Vamos a crear una instancia de esta nueva clase en nuestro ViewController:

Creamos la propiedad:

var characterDetailCoordinator: CharacterDetailPushCoordinator?
Creamos una propiedad de nuestro Coordinator en el ViewController

Y ahora sustituimos el código de navegación que estaba directamente en el ViewController:

tableViewDelegate?.didTapOnCell = { [weak self] index in
    print("Index \(index)")
    // Present New View Controller
    guard let dataSource = self?.tableViewDataSource else {
        return
    }
    let characterModel = dataSource.characters[index]
    self?.characterDetailCoordinator = CharacterDetailPushCoordinator(characterModel: characterModel,
                                                                     navigationController: self?.navigationController)
    self?.characterDetailCoordinator?.start()
}
Sustituimos la lógica que había en nuestro ViewController por nuestro Coordinator

Si compilamos vemos que funciona perfectamente, y que hemos movido la responsabilidad de la navegación a otra clase. A otra clase que su única responsabilidad es esa, la navegación. Imagina si es potente, que si ahora queremos presentar modalmente el CharacterDetail, podemos crear el siguiente Coordinator, lo vamos a llamar CharacterDetailModalCoordinator

import Foundation
import UIKit

final class CharacterDetailModalCoordinator: Coordinator {
    let characterModel: CharacterModel
    var viewController: UIViewController?
    
    init(characterModel: CharacterModel, viewController: UIViewController?) {
        self.characterModel = characterModel
        self.viewController = viewController
    }
    
    func start() {
        let characterDetailViewController = CharacterDetailViewController(characterDetailModel: characterModel)
        viewController?.present(characterDetailViewController, animated: true)
    }
}
Creamos un nuevo Coordinator para realizar la navegación de forma Modal

Y lo único que tendríamos que hacer en nuestro ViewController es crear la propiedad:

var characterDetailCoordinator: CharacterDetailModalCoordinator?
Creamos una instancia en nuestro ViewController

Y cambiar el PushCoordinator que teníamos, por el nuevo que acabamos de crear:

tableViewDelegate?.didTapOnCell = { [weak self] index in
    print("Index \(index)")
    // Present New View Controller
    guard let dataSource = self?.tableViewDataSource else {
        return
    }
    let characterModel = dataSource.characters[index]
    self.characterDetailCoordinator = CharacterDetailModalCoordinator(characterModel: characterModel,
                                                                     viewController: self)
    self.characterDetailCoordinator?.start()
}
Usamos nuestro nuevo Coordinator para presentar el ViewController de forma Modal

No hemos tenido que modificar lógica en nuestro ViewController, ya que hemos creado una ModalCoordinator que se encarga de presentar el ViewController.

Si compilamos, vemos que ahora al pulsar una celda, se presenta modalmente el CharacterDetailViewController.

Inyectar dependencias en el CharactersListViewController

Quizás es pronto para hablar de esto, pero voy a empezar a introducirlo. A mi me encanta sacar responsabilidades en clases, y que estas clases conformen un protocolo, una interfaz o también lo puedes llamar contrato. De esta manera, al crear los tests es muy fácil mockear (ojo que esto es un término nuevo que aún no había sacado en el canal).
En el caso de la vistas, están en una clase aparte y podríamos hacer snapshots tests directamente usando CharactersListView o CharacterDetailView (esto lo veremos en próximos videos).

En CharactersListViewController podríamos dejar de usar el Storyboard, e instanciar el ViewController con un init todas sus propiedades, de esta manera podríamos inyectar mocks para nuestros tests. Pero no te preocupes, que esto lo veremos en otros videos.

Conclusión

Hoy hemos aprendido a cómo usar el Patrón Coordinator dentro de la arquitectura Model-View-Controller. Este Patrón nos permite extraer la responsabilidad de navegación que añadiamos en nuestro ViewController para navegar a otro. Esta responsabilidad está encapsulada dentro de un Coordinator que se encarga de navegar a otras pantallas.