Coordinator: conceptos clave, patrón coordinador para navegación iOS

Autor: IT Sectr Publicado: 2026-02-18 Tiempo de lectura: 9 min

Coordinator (coordinador) es un patrón arquitectónico de navegación que traslada la lógica de transiciones entre pantallas desde ViewController a clases separadas. El patrón fue propuesto por Soroush Khanlou en 2015 y ha ganado amplia adopción en la comunidad iOS. Coordinator gestiona el flujo de la aplicación: crea y muestra ViewController, transfiere datos entre pantallas y maneja la finalización del flujo. El patrón resuelve el problema del Massive View Controller, extrayendo la navegación del controlador. Más información en el artículo original sobre Coordinator.

Puntos clave

  • Coordinator — patrón de navegación que extrae la lógica de transiciones de ViewController
  • Separación de responsabilidades — ViewController gestiona la UI, Coordinator la navegación
  • Router — componente auxiliar de Coordinator para abstraer UINavigationController
  • Flow — secuencia de pantallas gestionada por un Coordinator (ej., onboarding)
  • Delegación — los coordinadores hijos informan al padre mediante delegate/protocol

Qué es Coordinator: esencia del patrón de navegación

Coordinator es un patrón que asume la responsabilidad de la navegación en una aplicación iOS. En UIKit estándar, el ViewController gestiona las transiciones: present, push, show segue — todos los métodos de navegación se llaman desde UIViewController. Coordinator extrae esta lógica: el ViewController informa de un evento (por ejemplo, «el usuario hizo clic en el botón de inicio de sesión»), Coordinator decide qué pantalla mostrar a continuación. El ViewController se queda solo con la lógica de UI y delega la navegación al coordinador.

Estructura del patrón — CoordinatorProtocol con métodos start() y finish(). start() — inicio del flujo: creación del primer ViewController y su visualización. finish() — finalización del flujo con notificación al coordinador padre. Router — envoltorio sobre UINavigationController (o UISplitViewController), que proporciona métodos show, push, pop, dismiss. El coordinador no trabaja directamente con UINavigationController — solo a través de Router. Esto permite probar la navegación y cambiar el framework de UI.

ComponenteRolEjemplo
CoordinatorGestión del flujo de navegaciónAuthCoordinator, ProfileCoordinator
RouterAbstracción sobre UINavigationControllerpush, present, pop, dismiss
ViewControllerUI + delegación de eventos al CoordinatorLoginViewController.delegate

Problemas que resuelve Coordinator — Massive View Controller (la navegación es una causa común del crecimiento del controlador). En UIKit estándar, ViewController contiene prepareForSegue, delegados de navegación, manejo de unwind segues. Coordinator elimina esto. Los segues en storyboard son conexiones estáticas entre pantallas, Coordinator proporciona navegación dinámica con condiciones. La prueba de navegación se vuelve posible: se puede probar Coordinator sin UI verificando la secuencia de llamadas a Router.

Coordinator en Swift: implementación con Router y Flow

Coordinator básico en Swift — un protocolo con un tipo asociado para Router y métodos start/finish. Router — un protocolo que abstrae UINavigationController. Una implementación concreta de Router envuelve UINavigationController y le delega métodos. Coordinator acepta Router en init y lo usa para la navegación. Los coordinadores hijos se almacenan en el array childCoordinators para la gestión del ciclo de vida.

swift
// Router — abstracción de navegación
protocol RouterProtocol: AnyObject {
    func push(_ viewController: UIViewController, animated: Bool)
    func pop(animated: Bool)
    func present(_ viewController: UIViewController, animated: Bool)
    func dismiss(animated: Bool)
}

final class NavigationRouter: RouterProtocol {
    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func push(_ vc: UIViewController, animated: Bool) {
        navigationController.pushViewController(vc, animated: animated)
    }

    func pop(animated: Bool) {
        navigationController.popViewController(animated: animated)
    }

    func present(_ vc: UIViewController, animated: Bool) {
        navigationController.present(vc, animated: animated)
    }

    func dismiss(animated: Bool) {
        navigationController.dismiss(animated: animated)
    }
}

// Coordinator — gestión de flujo
protocol CoordinatorProtocol: AnyObject {
    var childCoordinators: [CoordinatorProtocol] { get set }
    var router: RouterProtocol { get }
    func start()
    func finish()
}

class AuthCoordinator: CoordinatorProtocol {
    var childCoordinators: [CoordinatorProtocol] = []
    let router: RouterProtocol

    init(router: RouterProtocol) {
        self.router = router
    }

    func start() {
        let loginVC = LoginViewController()
        loginVC.onLogin = { [weak self] in
            self?.showHome()
        }
        router.push(loginVC, animated: true)
    }

    private func showHome() {
        let homeCoordinator = HomeCoordinator(router: router)
        childCoordinators.append(homeCoordinator)
        homeCoordinator.start()
    }

    func finish() {
        childCoordinators.removeAll()
        router.pop(animated: true)
    }
}

Creación de Coordinator en AppDelegate/SceneDelegate — AppDelegate o SceneDelegate crea UINavigationController, lo envuelve en NavigationRouter, crea un Coordinator raíz (AppCoordinator) y llama a start(). AppCoordinator decide si mostrar onboarding, inicio de sesión o la pantalla principal — según el estado de la aplicación. Coordinator es el único punto de entrada para la navegación, ViewController no conoce otras pantallas.

Coordinadores hijos y jerarquía de coordinadores

Jerarquía de coordinadores — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. El coordinador hijo es creado por el padre y almacenado en el array childCoordinators. Cuando el coordinador hijo completa su trabajo, llama a finish() en el padre, y el padre lo elimina de childCoordinators. Esto previene fugas de memoria: Coordinator tiene una referencia fuerte a ViewController (a través de Router), y sin eliminación de childCoordinators, el objeto no se liberará.

swift
// Delegado para comunicación Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
    func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}

class AuthCoordinator: CoordinatorProtocol {
    weak var delegate: AuthCoordinatorDelegate?

    func finish() {
        delegate?.authCoordinatorFinished(self)
    }
}

// AppCoordinator — padre
class AppCoordinator: AuthCoordinatorDelegate {
    func startAuthFlow() {
        let authCoordinator = AuthCoordinator(router: router)
        authCoordinator.delegate = self
        childCoordinators.append(authCoordinator)
        authCoordinator.start()
    }

    func authCoordinatorFinished(_ coordinator: AuthCoordinator) {
        childCoordinators.removeAll { $0 is AuthCoordinator }
        startMainFlow()
    }
}

Gestión de childCoordinators — eliminar un Coordinator del array es la única forma de liberarlo. Si olvida eliminar un Coordinator completado, permanece en la memoria junto con sus ViewControllers. Enfoques recomendados: didMove(toParent:) del padre, callback de finalización o publicador Combine para eliminación automática. El patrón Coordinator no especifica el mecanismo de notificación — delegado, closure o Combine — la elección es del desarrollador.

Formas de transferir datos entre coordinadores

Transferencia de datos mediante delegado — el Coordinator hijo define un protocolo delegado con métodos a través de los cuales se pasan los resultados: func authCoordinator(_:didLoginWith user: User). El padre implementa el protocolo y recibe los datos al completar el flujo hijo. Esto es type-safe y explícito. Inconveniente: cada Coordinator hijo requiere un protocolo separado. Para proyectos con 10+ Coordinadores, esto lleva a un aumento de archivos.

Transferencia de datos mediante tipo Result — el método finish acepta Result, donde Output es un tipo genérico del resultado del flujo. Coordinator — un genérico con tipo de resultado asociado. start() con callback: start(completion: @escaping (Output) -> Void). Esto reduce el código: no es necesario escribir un protocolo separado para cada coordinador. RxSwift/Combine: Coordinator publica el resultado mediante PassthroughSubject/Publisher. La elección depende del enfoque arquitectónico del equipo.

MétodoVentajasDesventajas
DelegateType-safe, explícito, protocolos separadosMuchos protocolos, mucho boilerplate
ClosureCompacto, menos archivosDifícil depurar retain cycle
Combine/RxReactivo, fácil de combinarDependencia de librería, más difícil depurar

Capa de datos compartida — los Coordinadores no transfieren datos directamente, sino que usan un servicio/repositorio compartido. AuthCoordinator guarda el token en Keychain/UserDefaults, ProfileCoordinator lee desde allí. Los Coordinadores se comunican a través del estado compartido (contenedor de Dependency Injection) en lugar de llamadas directas. Esto reduce el acoplamiento entre Coordinadores, pero crea dependencias implícitas del estado compartido.

Comparación de Coordinator con Router, VIPER y MVVM-C

Coordinator vs Router — Router es un componente de Coordinator que abstrae UINavigationController. Coordinator es responsable del flujo (qué pantalla mostrar), Router — de la mecánica (cómo mostrar: push/present). Router es el «cómo», Coordinator es el «qué». Se puede usar Router sin Coordinator (por ejemplo, Navigator singleton), pero Coordinator sin Router es solo un ViewController con otra abstracción. Generalmente ambos patrones se usan juntos.

Coordinator vs VIPER — VIPER tiene un componente Wireframe responsable de la navegación — análogo a Coordinator. En VIPER, Wireframe es parte del módulo, Coordinator es una capa separada sobre los módulos. Un módulo VIPER (View-Interactor-Presenter-Entity-Router) incluye la navegación como parte del módulo. Coordinator es externo a los módulos: los crea y los conecta, pero no forma parte de ellos. Coordinator es más flexible para reutilizar pantallas en diferentes flujos.

MVVM-C — una extensión de MVVM con Coordinator. ViewModel no conoce directamente a Coordinator — ViewController delega la navegación a través de ViewModel, ViewModel llama al coordinator mediante un protocolo. MVVM-C es el enfoque estándar para proyectos iOS con SwiftUI: Coordinator gestiona NavigationStack o fullScreenCover, ViewModel llama al coordinator publicando estado. Apple no recomienda Coordinator para SwiftUI — NavigationStack y NavigationPath son mecanismos de navegación incorporados.

swift
// MVVM-C: ViewModel llama a Coordinator mediante un protocolo
protocol AuthNavigationProtocol: AnyObject {
    func showMainScreen()
    func showForgotPassword()
}

class AuthViewModel: ObservableObject {
    weak var navigation: AuthNavigationProtocol?

    func loginTapped() {
        // lógica...
        navigation?.showMainScreen()
    }
}

Preguntas frecuentes

¿Se necesita Coordinator para SwiftUI?

Para SwiftUI, la navegación incorporada (NavigationStack, NavigationPath) a menudo reemplaza a Coordinator. Apple recomienda la navegación basada en path. Coordinator tiene sentido para flujos complejos con condiciones profundas (onboarding-inicio de sesión-pantalla principal según el rol). Para aplicaciones simples en SwiftUI, Coordinator es redundante — use NavigationPath.

¿Coordinator es un Router?

No, son patrones diferentes. Coordinator gestiona el flujo de navegación: decide qué pantalla mostrar, crea ViewControllers y los conecta. Router es una abstracción sobre UINavigationController: push, present, pop, dismiss. Coordinator usa Router para realizar la navegación. En algunas implementaciones, Router incluye lógica de Coordinator (Router-per-screen), pero esto se desvía del patrón original.

¿Cómo evitar el retain cycle en Coordinator?

Dos fuentes principales de fugas: childCoordinators (el padre retiene al hijo, olvidando eliminarlo) y Router (UINavigationController retiene a ViewController). Solución: elimine siempre al Coordinator hijo del array al hacer finish(). Use referencia weak para el delegate. Para Router — no mantenga una referencia fuerte a UINavigationController si ya está en la jerarquía de window. Pruebe el deinit del Coordinator.

¿Cuándo es Coordinator excesivo?

Para aplicaciones con 3-5 pantallas, Coordinator es excesivo — segue o simple navigationController.pushViewController es más fácil. Para aplicaciones SwiftUI con NavigationStack — también redundante. Coordinator se justifica para aplicaciones con 15+ pantallas, flujos complejos (onboarding con ramificaciones, autorización con recuperación de contraseña) y proyectos mixtos UIKit/SwiftUI.

¿Cómo probar Coordinator?

Mock Router — verifique qué métodos se llaman y con qué parámetros. Verifique childCoordinators: después de start() el array no está vacío, después de finish() — vacío. Coordinator se prueba sin UI: Router es un protocolo, su mock no requiere UIKit. Use XCTestExpectation para flujo asíncrono. En Android — pruebas similares de NavigationController y NavHost con navegación mock.

Resumen

  • Coordinator — patrón de navegación que extrae las transiciones de ViewController a una clase separada
  • Router — abstracción de UINavigationController utilizada por Coordinator
  • Jerarquía — Coordinadores padres e hijos con delegación de resultados
  • MVVM-C — enfoque estándar para proyectos UIKit con Coordinator
  • SwiftUI — navegación incorporada mediante NavigationStack reemplaza Coordinator
  • Pruebas — Coordinator se prueba mediante mock Router sin UIKit

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también