Coordinator: conceitos-chave, padrão coordenador para navegação iOS

Autor: IT Sectr Publicado: 2026-02-18 Tempo de leitura: 9 min

Coordinator (coordenador) — um padrão arquitetural de navegação que move a lógica de transições entre telas do ViewController para classes separadas. O padrão foi proposto por Soroush Khanlou em 2015 e ganhou ampla adoção na comunidade iOS. O Coordinator gerencia o fluxo da aplicação: cria e exibe ViewController, passa dados entre telas e lida com a conclusão do fluxo. O padrão resolve o problema do Massive View Controller, extraindo a navegação do controlador. Saiba mais no artigo original sobre Coordinator.

Pontos principais

  • Coordinator — padrão de navegação que extrai a lógica de transições do ViewController
  • Separação de responsabilidades — ViewController gerencia a UI, Coordinator a navegação
  • Router — componente auxiliar do Coordinator para abstrair UINavigationController
  • Flow — sequência de telas gerenciada por um Coordinator (ex., onboarding)
  • Delegação — coordenadores filhos reportam ao pai via delegate/protocol

O que é Coordinator: essência do padrão de navegação

Coordinator — um padrão que assume a responsabilidade pela navegação em uma aplicação iOS. No UIKit padrão, o próprio ViewController gerencia as transições: present, push, show segue — todos os métodos de navegação são chamados a partir do UIViewController. O Coordinator extrai essa lógica: o ViewController relata um evento (por exemplo, «o usuário clicou no botão de login»), o Coordinator decide qual tela mostrar em seguida. O ViewController fica apenas com a lógica de UI e delega a navegação ao coordenador.

Estrutura do padrão — CoordinatorProtocol com métodos start() e finish(). start() — início do fluxo: criação do primeiro ViewController e exibição. finish() — conclusão do fluxo com notificação ao coordenador pai. Router — um invólucro sobre UINavigationController (ou UISplitViewController), fornecendo métodos show, push, pop, dismiss. O coordenador não trabalha diretamente com UINavigationController — apenas através do Router. Isso permite testar a navegação e trocar o framework de UI.

ComponenteFunçãoExemplo
CoordinatorGerenciamento do fluxo de navegaçãoAuthCoordinator, ProfileCoordinator
RouterAbstração sobre UINavigationControllerpush, present, pop, dismiss
ViewControllerUI + delegação de eventos ao CoordinatorLoginViewController.delegate

Problemas que o Coordinator resolve — Massive View Controller (a navegação é uma causa comum de inchaço do controlador). No UIKit padrão, o ViewController contém prepareForSegue, delegados de navegação, manipulação de unwind segues. O Coordinator elimina isso. As segues no storyboard são conexões estáticas entre telas, o Coordinator fornece navegação dinâmica com condições. Testar a navegação torna-se possível: pode-se testar o Coordinator sem UI verificando a sequência de chamadas ao Router.

Coordinator em Swift: implementação com Router e Flow

Coordinator básico em Swift — um protocolo com um tipo associado para Router e métodos start/finish. Router — um protocolo que abstrai UINavigationController. Uma implementação concreta de Router envolve UINavigationController e delega métodos a ele. O Coordinator aceita Router no init e o utiliza para navegação. Os coordenadores filhos são armazenados no array childCoordinators para gerenciamento do ciclo de vida.

swift
// Router — abstração de navegação
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 — gerenciamento de fluxo
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)
    }
}

Criação do Coordinator no AppDelegate/SceneDelegate — AppDelegate ou SceneDelegate cria UINavigationController, envolve-o em NavigationRouter, cria um Coordinator raiz (AppCoordinator) e chama start(). O AppCoordinator decide se deve mostrar onboarding, login ou a tela principal — dependendo do estado da aplicação. Coordinator é o único ponto de entrada para navegação, ViewController não conhece outras telas.

Coordenadores filhos e hierarquia de coordenadores

Hierarquia de coordenadores — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. O coordenador filho é criado pelo pai e armazenado no array childCoordinators. Quando o coordenador filho conclui seu trabalho, ele chama finish() no pai, e o pai o remove de childCoordinators. Isso evita vazamentos de memória: o Coordinator tem uma referência forte ao ViewController (através do Router), e sem remoção de childCoordinators, o objeto não será liberado.

swift
// Delegate para comunicação Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
    func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}

class AuthCoordinator: CoordinatorProtocol {
    weak var delegate: AuthCoordinatorDelegate?

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

// AppCoordinator — pai
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()
    }
}

Gerenciamento de childCoordinators — remover um Coordinator do array é a única maneira de liberá-lo. Se você esquecer de remover um Coordinator concluído, ele permanece na memória junto com seus ViewControllers. Abordagens recomendadas: didMove(toParent:) do pai, callback de conclusão ou publisher Combine para remoção automática. O padrão Coordinator não especifica o mecanismo de notificação — delegate, closure ou Combine — a escolha é do desenvolvedor.

Formas de transferir dados entre coordenadores

Transferência de dados via delegate — o Coordinator filho define um protocolo delegate com métodos através dos quais os resultados são passados: func authCoordinator(_:didLoginWith user: User). O pai implementa o protocolo e recebe os dados ao concluir o fluxo filho. Isso é type-safe e explícito. Desvantagem: cada Coordinator filho requer um protocolo separado. Para projetos com 10+ Coordenadores, isso leva a um aumento de arquivos.

Transferência de dados via tipo Result — o método finish aceita Result, onde Output é um tipo genérico do resultado do fluxo. Coordinator — um genérico com tipo de resultado associado. start() com callback: start(completion: @escaping (Output) -> Void). Isso reduz o código: não é necessário escrever um protocolo separado para cada coordenador. RxSwift/Combine: Coordinator publica o resultado via PassthroughSubject/Publisher. A escolha depende da abordagem arquitetural da equipe.

MétodoVantagensDesvantagens
DelegateType-safe, explícito, protocolos separadosMuitos protocolos, muito boilerplate
ClosureCompacto, menos arquivosDifícil depurar retain cycle
Combine/RxReativo, fácil de combinarDependência de biblioteca, mais difícil depurar

Camada de dados compartilhada — os Coordenadores não transferem dados diretamente, mas usam um serviço/repositório compartilhado. AuthCoordinator salva o token no Keychain/UserDefaults, ProfileCoordinator lê de lá. Os Coordenadores comunicam-se através do estado compartilhado (contêiner de Dependency Injection) em vez de chamadas diretas. Isso reduz o acoplamento entre Coordenadores, mas cria dependências implícitas do estado compartilhado.

Comparação do Coordinator com Router, VIPER e MVVM-C

Coordinator vs Router — Router é um componente do Coordinator que abstrai UINavigationController. Coordinator é responsável pelo fluxo (qual tela mostrar), Router — pela mecânica (como mostrar: push/present). Router é o «como», Coordinator é o «o quê». Pode-se usar Router sem Coordinator (ex., Navigator singleton), mas Coordinator sem Router é apenas um ViewController com uma abstração diferente. Geralmente ambos os padrões são usados juntos.

Coordinator vs VIPER — VIPER tem um componente Wireframe responsável pela navegação — análogo ao Coordinator. No VIPER, Wireframe faz parte do módulo, Coordinator é uma camada separada sobre os módulos. Um módulo VIPER (View-Interactor-Presenter-Entity-Router) inclui a navegação como parte do módulo. Coordinator é externo aos módulos: ele os cria e conecta, mas não faz parte deles. Coordinator é mais flexível para reutilizar telas em diferentes fluxos.

MVVM-C — uma extensão do MVVM com Coordinator. O ViewModel não conhece diretamente o Coordinator — o ViewController delega a navegação através do ViewModel, o ViewModel chama o coordinator através de um protocolo. MVVM-C é a abordagem padrão para projetos iOS com SwiftUI: Coordinator gerencia NavigationStack ou fullScreenCover, ViewModel chama o coordinator publicando estado. A Apple não recomenda Coordinator para SwiftUI — NavigationStack e NavigationPath são mecanismos de navegação incorporados.

swift
// MVVM-C: ViewModel chama Coordinator através de um protocolo
protocol AuthNavigationProtocol: AnyObject {
    func showMainScreen()
    func showForgotPassword()
}

class AuthViewModel: ObservableObject {
    weak var navigation: AuthNavigationProtocol?

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

Perguntas frequentes

Precisa de Coordinator para SwiftUI?

Para SwiftUI, a navegação incorporada (NavigationStack, NavigationPath) muitas vezes substitui o Coordinator. A Apple recomenda navegação baseada em caminho (path-based). Coordinator faz sentido para fluxos complexos com condições profundas (onboarding-login-tela principal dependendo do papel). Para aplicações SwiftUI simples, Coordinator é redundante — use NavigationPath.

Coordinator é um Router?

Não, são padrões diferentes. Coordinator gerencia o fluxo de navegação: decide qual tela mostrar, cria ViewControllers e os conecta. Router é uma abstração sobre UINavigationController: push, present, pop, dismiss. Coordinator usa Router para realizar a navegação. Em algumas implementações, Router inclui lógica de Coordinator (Router-per-screen), mas isso desvia do padrão original.

Como evitar retain cycle no Coordinator?

Duas fontes principais de vazamentos: childCoordinators (o pai retém o filho, esquecendo de removê-lo) e Router (UINavigationController retém ViewController). Solução: sempre remova o Coordinator filho do array ao fazer finish(). Use referência weak para o delegate. Para Router — não mantenha referência forte a UINavigationController se ele já estiver na hierarquia da window. Teste o deinit do Coordinator.

Quando Coordinator é exagerado?

Para aplicações com 3-5 telas, Coordinator é exagerado — segue ou simples navigationController.pushViewController é mais fácil. Para aplicações SwiftUI com NavigationStack — também redundante. Coordinator justifica-se para aplicações com 15+ telas, fluxos complexos (onboarding com ramificações, autorização com recuperação de senha) e projetos mistos UIKit/SwiftUI.

Como testar Coordinator?

Mock Router — verifique quais métodos são chamados e com quais parâmetros. Verifique childCoordinators: após start() o array não está vazio, após finish() — vazio. Coordinator é testado sem UI: Router é um protocolo, seu mock não requer UIKit. Use XCTestExpectation para fluxo assíncrono. No Android — teste semelhante de NavigationController e NavHost com navegação mock.

Resumo

  • Coordinator — padrão de navegação que extrai transições do ViewController para uma classe separada
  • Router — abstração de UINavigationController usada pelo Coordinator
  • Hierarquia — Coordenadores pais e filhos com delegação de resultados
  • MVVM-C — abordagem padrão para projetos UIKit com Coordinator
  • SwiftUI — navegação incorporada via NavigationStack substitui Coordinator
  • Testes — Coordinator é testado através de mock Router sem UIKit

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também