Coordinator (coordinatore) — un pattern architetturale di navigazione che sposta la logica delle transizioni tra schermate dal ViewController a classi separate. Il pattern è stato proposto da Soroush Khanlou nel 2015 e ha ottenuto ampia diffusione nella comunità iOS. Il Coordinator gestisce il flusso dell'applicazione: crea e visualizza il ViewController, passa dati tra le schermate e gestisce il completamento del flusso. Il pattern risolve il problema del Massive View Controller estraendo la navigazione dal controller. Maggiori informazioni nell'articolo originale su Coordinator.
Punti chiave
Coordinator — un pattern che si assume la responsabilità della navigazione in un'applicazione iOS. Nell'UIKit standard, il ViewController stesso gestisce le transizioni: present, push, show segue — tutti i metodi di navigazione vengono chiamati da UIViewController. Il Coordinator estrae questa logica: il ViewController segnala un evento (ad esempio, «l'utente ha cliccato il pulsante di login»), il Coordinator decide quale schermata mostrare successivamente. Il ViewController rimane solo con la logica dell'interfaccia e delega la navigazione al coordinatore.
Struttura del pattern — CoordinatorProtocol con i metodi start() e finish(). start() — inizio del flusso: creazione del primo ViewController e visualizzazione. finish() — completamento del flusso con notifica al coordinatore genitore. Router — un wrapper attorno a UINavigationController (o UISplitViewController), che fornisce i metodi show, push, pop, dismiss. Il coordinatore non lavora direttamente con UINavigationController — solo tramite Router. Ciò consente di testare la navigazione e cambiare il framework dell'interfaccia.
| Componente | Ruolo | Esempio |
|---|---|---|
| Coordinator | Gestione del flusso di navigazione | AuthCoordinator, ProfileCoordinator |
| Router | Astrazione su UINavigationController | push, present, pop, dismiss |
| ViewController | UI + delega eventi al Coordinator | LoginViewController.delegate |
Problemi risolti da Coordinator — Massive View Controller (la navigazione è una causa comune di ingrossamento del controller). Nell'UIKit standard, ViewController contiene prepareForSegue, delegati di navigazione, gestione degli unwind segues. Coordinator elimina tutto ciò. I segues nello storyboard sono connessioni statiche tra schermate, Coordinator fornisce navigazione dinamica con condizioni. Testare la navigazione diventa possibile: si può testare Coordinator senza UI verificando la sequenza delle chiamate al Router.
Coordinator base in Swift — un protocollo con un tipo associato per Router e i metodi start/finish. Router — un protocollo che astrae UINavigationController. Un'implementazione concreta di Router avvolge UINavigationController e gli delega i metodi. Coordinator accetta Router nell'init e lo utilizza per la navigazione. I coordinatori figli vengono memorizzati nell'array childCoordinators per la gestione del ciclo di vita.
// Router — astrazione di navigazione
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 — gestione del flusso
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)
}
}
Creazione del Coordinator in AppDelegate/SceneDelegate — AppDelegate o SceneDelegate crea UINavigationController, lo avvolge in NavigationRouter, crea un Coordinator radice (AppCoordinator) e chiama start(). AppCoordinator decide se mostrare onboarding, login o la schermata principale — in base allo stato dell'applicazione. Coordinator è il punto di ingresso unico per la navigazione, ViewController non conosce altre schermate.
Gerarchia dei coordinatori — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Il coordinatore figlio viene creato dal genitore e memorizzato nell'array childCoordinators. Quando il coordinatore figlio completa il suo lavoro, chiama finish() sul genitore, e il genitore lo rimuove da childCoordinators. Questo previene le fughe di memoria: Coordinator ha un riferimento forte al ViewController (tramite Router), e senza rimozione da childCoordinators, l'oggetto non verrà rilasciato.
// Delegate per comunicazione Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}
class AuthCoordinator: CoordinatorProtocol {
weak var delegate: AuthCoordinatorDelegate?
func finish() {
delegate?.authCoordinatorFinished(self)
}
}
// AppCoordinator — genitore
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()
}
}
Gestione di childCoordinators — rimuovere un Coordinator dall'array è l'unico modo per rilasciarlo. Se si dimentica di rimuovere un Coordinator completato, rimane in memoria insieme ai suoi ViewController. Approcci consigliati: didMove(toParent:) del genitore, callback al completamento o publisher Combine per la rimozione automatica. Il pattern Coordinator non specifica il meccanismo di notifica — delegate, closure o Combine — la scelta spetta allo sviluppatore.
Trasferimento dati tramite delegato — il Coordinator figlio definisce un protocollo delegato con metodi attraverso cui vengono passati i risultati: func authCoordinator(_:didLoginWith user: User). Il genitore implementa il protocollo e riceve i dati al completamento del flusso figlio. Questo è type-safe ed esplicito. Svantaggio: ogni Coordinator figlio richiede un protocollo separato. Per progetti con 10+ Coordinator, ciò porta a un aumento dei file.
Trasferimento dati tramite tipo Result — il metodo finish accetta Result
| Metodo | Vantaggi | Svantaggi |
|---|---|---|
| Delegate | Type-safe, esplicito, protocolli separati | Molti protocolli, molto boilerplate |
| Closure | Compatto, meno file | Difficile debuggare retain cycle |
| Combine/Rx | Reattivo, facile da combinare | Dipende da librerie, più difficile da debuggare |
Livello dati condiviso — i Coordinator non trasferiscono dati direttamente ma utilizzano un servizio/repository condiviso. AuthCoordinator salva il token in Keychain/UserDefaults, ProfileCoordinator legge da lì. I Coordinator comunicano attraverso lo stato condiviso (contenitore di Dependency Injection) invece di chiamate dirette. Questo riduce l'accoppiamento tra Coordinator ma crea dipendenze implicite dallo stato condiviso.
Coordinator vs Router — Router è un componente di Coordinator che astrae UINavigationController. Coordinator è responsabile del flusso (quale schermata mostrare), Router — della meccanica (come mostrare: push/present). Router è il «come», Coordinator è il «cosa». Si può usare Router senza Coordinator (es., Navigator singleton), ma Coordinator senza Router è solo un ViewController con un'astrazione diversa. Di solito entrambi i pattern vengono usati insieme.
Coordinator vs VIPER — VIPER ha un componente Wireframe responsabile della navigazione — analogo a Coordinator. In VIPER, Wireframe fa parte del modulo, Coordinator è un livello separato sopra i moduli. Un modulo VIPER (View-Interactor-Presenter-Entity-Router) include la navigazione come parte del modulo. Coordinator è esterno ai moduli: li crea e li collega, ma non ne fa parte. Coordinator è più flessibile per il riutilizzo delle schermate in diversi flussi.
MVVM-C — un'estensione di MVVM con Coordinator. Il ViewModel non conosce direttamente Coordinator — il ViewController delega la navigazione tramite ViewModel, il ViewModel chiama il coordinator tramite un protocollo. MVVM-C è l'approccio standard per progetti iOS con SwiftUI: Coordinator gestisce NavigationStack o fullScreenCover, il ViewModel chiama il coordinator pubblicando lo stato. Apple non raccomanda Coordinator per SwiftUI — NavigationStack e NavigationPath sono meccanismi di navigazione incorporati.
// MVVM-C: ViewModel chiama Coordinator tramite protocollo
protocol AuthNavigationProtocol: AnyObject {
func showMainScreen()
func showForgotPassword()
}
class AuthViewModel: ObservableObject {
weak var navigation: AuthNavigationProtocol?
func loginTapped() {
// logica...
navigation?.showMainScreen()
}
}
Domande frequenti
Per SwiftUI, la navigazione incorporata (NavigationStack, NavigationPath) spesso sostituisce Coordinator. Apple raccomanda la navigazione basata sul percorso (path-based). Coordinator ha senso per flussi complessi con condizioni profonde (onboarding-login-schermata principale in base al ruolo). Per applicazioni SwiftUI semplici, Coordinator è ridondante — usa NavigationPath.
No, sono pattern diversi. Coordinator gestisce il flusso di navigazione: decide quale schermata mostrare, crea ViewController e li collega. Router è un'astrazione su UINavigationController: push, present, pop, dismiss. Coordinator usa Router per eseguire la navigazione. In alcune implementazioni, Router include logica di Coordinator (Router-per-screen), ma questo si discosta dal pattern originale.
Due fonti principali di perdite: childCoordinators (il genitore trattiene il figlio, dimenticando di rimuoverlo) e Router (UINavigationController trattiene ViewController). Soluzione: rimuovere sempre il Coordinator figlio dall'array al finish(). Usare un riferimento weak per il delegato. Per Router — non mantenere un riferimento forte a UINavigationController se è già nella gerarchia della finestra. Testare il deinit del Coordinator.
Per applicazioni con 3-5 schermate, Coordinator è eccessivo — segue o semplice navigationController.pushViewController è più facile. Per applicazioni SwiftUI con NavigationStack — anche ridondante. Coordinator si giustifica per applicazioni con 15+ schermate, flussi complessi (onboarding con ramificazioni, autorizzazione con recupero password) e progetti misti UIKit/SwiftUI.
Mock Router — verificare quali metodi vengono chiamati e con quali parametri. Verificare childCoordinators: dopo start() l'array non è vuoto, dopo finish() — vuoto. Coordinator viene testato senza UI: Router è un protocollo, il suo mock non richiede UIKit. Usare XCTestExpectation per flussi asincroni. In Android — test simile di NavigationController e NavHost con navigazione mock.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche