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 — 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.
| Componente | Função | Exemplo |
|---|---|---|
| Coordinator | Gerenciamento do fluxo de navegação | AuthCoordinator, ProfileCoordinator |
| Router | Abstração sobre UINavigationController | push, present, pop, dismiss |
| ViewController | UI + delegação de eventos ao Coordinator | LoginViewController.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 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.
// 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.
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.
// 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.
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
| Método | Vantagens | Desvantagens |
|---|---|---|
| Delegate | Type-safe, explícito, protocolos separados | Muitos protocolos, muito boilerplate |
| Closure | Compacto, menos arquivos | Difícil depurar retain cycle |
| Combine/Rx | Reativo, fácil de combinar | Dependê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.
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.
// 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
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.
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.
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.
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.
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
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.
Leia também