Coordinator: concetti chiave, pattern coordinatore per la navigazione iOS

Autore: IT Sectr Pubblicato: 2026-02-18 Tempo di lettura: 9 min

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 — pattern di navigazione che estrae la logica di transizione dal ViewController
  • Separazione delle responsabilità — ViewController gestisce l'UI, Coordinator la navigazione
  • Router — componente ausiliario di Coordinator per astrarre UINavigationController
  • Flow — sequenza di schermate gestita da un Coordinator (es. onboarding)
  • Delega — i coordinatori figli riferiscono al genitore tramite delegate/protocol

Cos'è Coordinator: essenza del pattern di navigazione

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.

ComponenteRuoloEsempio
CoordinatorGestione del flusso di navigazioneAuthCoordinator, ProfileCoordinator
RouterAstrazione su UINavigationControllerpush, present, pop, dismiss
ViewControllerUI + delega eventi al CoordinatorLoginViewController.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 in Swift: implementazione con Router e Flow

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.

swift
// 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.

Coordinator figli e gerarchia dei coordinatori

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.

swift
// 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.

Modalità di trasferimento dati tra coordinatori

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, dove Output è un tipo generico del risultato del flusso. Coordinator — un generico con tipo di risultato associato. start() con callback: start(completion: @escaping (Output) -> Void). Questo riduce il codice: non è necessario scrivere un protocollo separato per ogni coordinatore. RxSwift/Combine: Coordinator pubblica il risultato tramite PassthroughSubject/Publisher. La scelta dipende dall'approccio architetturale del team.

MetodoVantaggiSvantaggi
DelegateType-safe, esplicito, protocolli separatiMolti protocolli, molto boilerplate
ClosureCompatto, meno fileDifficile debuggare retain cycle
Combine/RxReattivo, facile da combinareDipende 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.

Confronto di Coordinator con Router, VIPER e MVVM-C

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.

swift
// 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

Serve Coordinator per SwiftUI?

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.

Coordinator è un Router?

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.

Come evitare i retain cycle in Coordinator?

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.

Quando Coordinator è eccessivo?

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.

Come testare Coordinator?

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

  • Coordinator — pattern di navigazione che estrae le transizioni dal ViewController in una classe separata
  • Router — astrazione di UINavigationController usata da Coordinator
  • Gerarchia — Coordinator genitori e figli con delega dei risultati
  • MVVM-C — approccio standard per progetti UIKit con Coordinator
  • SwiftUI — navigazione incorporata via NavigationStack sostituisce Coordinator
  • Test — Coordinator viene testato tramite mock Router senza UIKit

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.

Discuti il progetto

Leggi anche