Coordinator : concepts clés, modèle coordinateur pour la navigation iOS

Auteur : IT Sectr Publié le : 2026-02-18 Temps de lecture : 9 min

Coordinator (coordinateur) — un modèle architectural de navigation qui déplace la logique des transitions entre écrans du ViewController vers des classes séparées. Le modèle a été proposé par Soroush Khanlou en 2015 et a été largement adopté dans la communauté iOS. Le Coordinator gère le flux de l'application : crée et affiche le ViewController, transmet les données entre les écrans et gère la fin du flux. Le modèle résout le problème du Massive View Controller en extrayant la navigation du contrôleur. En savoir plus dans l'article original sur Coordinator.

Points essentiels

  • Coordinator — modèle de navigation qui extrait la logique de transition du ViewController
  • Séparation des responsabilités — ViewController gère l'UI, Coordinator la navigation
  • Router — composant auxiliaire de Coordinator pour abstraire UINavigationController
  • Flow — séquence d'écrans gérée par un Coordinator (ex. onboarding)
  • Délégation — les coordinateurs enfants rapportent au parent via delegate/protocol

Qu'est-ce que Coordinator : essence du modèle de navigation

Coordinator — un modèle qui prend la responsabilité de la navigation dans une application iOS. Dans UIKit standard, le ViewController lui-même gère les transitions : present, push, show segue — toutes les méthodes de navigation sont appelées depuis UIViewController. Le Coordinator extrait cette logique : le ViewController signale un événement (par exemple, « l'utilisateur a cliqué sur le bouton de connexion »), le Coordinator décide quel écran afficher ensuite. Le ViewController conserve uniquement la logique d'interface et délègue la navigation au coordinateur.

Structure du modèle — CoordinatorProtocol avec les méthodes start() et finish(). start() — début d'un flux : création du premier ViewController et affichage. finish() — fin du flux avec notification au coordinateur parent. Router — une enveloppe autour de UINavigationController (ou UISplitViewController), fournissant les méthodes show, push, pop, dismiss. Le coordinateur ne travaille pas directement avec UINavigationController — seulement via Router. Cela permet de tester la navigation et de changer le framework d'interface.

ComposantRôleExemple
CoordinatorGestion du flux de navigationAuthCoordinator, ProfileCoordinator
RouterAbstraction sur UINavigationControllerpush, present, pop, dismiss
ViewControllerUI + délégation d'événements au CoordinatorLoginViewController.delegate

Problèmes résolus par Coordinator — Massive View Controller (la navigation est une cause courante de gonflement du contrôleur). Dans UIKit standard, ViewController contient prepareForSegue, les délégués de navigation, la gestion des unwind segues. Coordinator élimine cela. Les segues dans le storyboard sont des connexions statiques entre écrans, Coordinator fournit une navigation dynamique avec conditions. Tester la navigation devient possible : on peut tester Coordinator sans UI en vérifiant la séquence des appels au Router.

Coordinator en Swift : implémentation avec Router et Flow

Coordinator de base en Swift — un protocole avec un type associé pour Router et les méthodes start/finish. Router — un protocole qui abstrait UINavigationController. Une implémentation concrète de Router enveloppe UINavigationController et lui délègue les méthodes. Coordinator accepte Router dans init et l'utilise pour la navigation. Les coordinateurs enfants sont stockés dans le tableau childCoordinators pour la gestion du cycle de vie.

swift
// Router — abstraction de navigation
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 — gestion de flux
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)
    }
}

Création du Coordinator dans AppDelegate/SceneDelegate — AppDelegate ou SceneDelegate crée UINavigationController, l'enveloppe dans NavigationRouter, crée un Coordinator racine (AppCoordinator) et appelle start(). AppCoordinator décide d'afficher l'onboarding, la connexion ou l'écran principal — selon l'état de l'application. Coordinator est le point d'entrée unique pour la navigation, ViewController ne connaît pas les autres écrans.

Coordinateurs enfants et hiérarchie des coordinateurs

Hiérarchie des coordinateurs — AppCoordinator → AuthCoordinator/MainCoordinator → ProfileCoordinator/SettingsCoordinator. Le coordinateur enfant est créé par le parent et stocké dans le tableau childCoordinators. Lorsque le coordinateur enfant termine son travail, il appelle finish() sur le parent, et le parent le retire de childCoordinators. Cela évite les fuites mémoire : Coordinator a une référence forte sur ViewController (via Router), et sans suppression de childCoordinators, l'objet ne sera pas libéré.

swift
// Délégué pour la communication Coordinator -> Parent
protocol AuthCoordinatorDelegate: AnyObject {
    func authCoordinatorFinished(_ coordinator: AuthCoordinator)
}

class AuthCoordinator: CoordinatorProtocol {
    weak var delegate: AuthCoordinatorDelegate?

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

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

Gestion des childCoordinators — supprimer un Coordinator du tableau est le seul moyen de le libérer. Si vous oubliez de supprimer un Coordinator terminé, il reste en mémoire avec ses ViewControllers. Approches recommandées : didMove(toParent:) du parent, callback à la fin, ou publisher Combine pour la suppression automatique. Le modèle Coordinator ne spécifie pas le mécanisme de notification — delegate, closure ou Combine — le choix appartient au développeur.

Moyens de transmettre des données entre coordinateurs

Transmission de données via délégation — le Coordinator enfant définit un protocole délégué avec des méthodes par lesquelles les résultats sont passés : func authCoordinator(_:didLoginWith user: User). Le parent implémente le protocole et reçoit les données à la fin du flux enfant. C'est type-safe et explicite. Inconvénient : chaque Coordinator enfant nécessite un protocole séparé. Pour les projets avec 10+ Coordinateurs, cela entraîne une augmentation du nombre de fichiers.

Transmission de données via le type Result — la méthode finish accepte Result, où Output est un type générique du résultat du flux. Coordinator — un générique avec un type de résultat associé. start() avec callback : start(completion: @escaping (Output) -> Void). Cela réduit le code : pas besoin d'écrire un protocole séparé pour chaque coordinateur. RxSwift/Combine : Coordinator publie le résultat via PassthroughSubject/Publisher. Le choix dépend de l'approche architecturale de l'équipe.

MéthodeAvantagesInconvénients
DelegateType-safe, explicite, protocoles séparésBeaucoup de protocoles, beaucoup de boilerplate
ClosureCompact, moins de fichiersDifficile de déboguer les retain cycles
Combine/RxRéactif, facile à combinerDépendance à la bibliothèque, plus difficile à déboguer

Couche de données partagée — les Coordinateurs ne transmettent pas les données directement mais utilisent un service/référentiel partagé. AuthCoordinator sauvegarde le token dans Keychain/UserDefaults, ProfileCoordinator lit depuis là. Les Coordinateurs communiquent via l'état partagé (conteneur d'injection de dépendances) plutôt que par des appels directs. Cela réduit le couplage entre Coordinateurs mais crée des dépendances implicites à l'état partagé.

Comparaison de Coordinator avec Router, VIPER et MVVM-C

Coordinator vs Router — Router est un composant de Coordinator qui abstrait UINavigationController. Coordinator est responsable du flux (quel écran afficher), Router — de la mécanique (comment afficher : push/present). Router est le « comment », Coordinator est le « quoi ». On peut utiliser Router sans Coordinator (ex., singleton Navigator), mais Coordinator sans Router n'est qu'un ViewController avec une abstraction différente. Généralement, les deux modèles sont utilisés ensemble.

Coordinator vs VIPER — VIPER a un composant Wireframe responsable de la navigation — analogue à Coordinator. Dans VIPER, Wireframe fait partie du module, Coordinator est une couche séparée au-dessus des modules. Un module VIPER (View-Interactor-Presenter-Entity-Router) inclut la navigation comme partie du module. Coordinator est externe aux modules : il les crée et les connecte, mais n'en fait pas partie. Coordinator est plus flexible pour réutiliser des écrans dans différents flux.

MVVM-C — une extension de MVVM avec Coordinator. Le ViewModel ne connaît pas directement Coordinator — le ViewController délègue la navigation via le ViewModel, le ViewModel appelle le coordinator via un protocole. MVVM-C est l'approche standard pour les projets iOS avec SwiftUI : Coordinator gère NavigationStack ou fullScreenCover, le ViewModel appelle le coordinator en publiant son état. Apple ne recommande pas Coordinator pour SwiftUI — NavigationStack et NavigationPath sont des mécanismes de navigation intégrés.

swift
// MVVM-C : ViewModel appelle Coordinator via un protocole
protocol AuthNavigationProtocol: AnyObject {
    func showMainScreen()
    func showForgotPassword()
}

class AuthViewModel: ObservableObject {
    weak var navigation: AuthNavigationProtocol?

    func loginTapped() {
        // logique...
        navigation?.showMainScreen()
    }
}

Questions fréquentes

A-t-on besoin de Coordinator pour SwiftUI ?

Pour SwiftUI, la navigation intégrée (NavigationStack, NavigationPath) remplace souvent Coordinator. Apple recommande la navigation basée sur le chemin (path-based). Coordinator a du sens pour les flux complexes avec des conditions profondes (onboarding-connexion-écran principal selon le rôle). Pour les applications SwiftUI simples, Coordinator est redondant — utilisez NavigationPath.

Coordinator est-il un Router ?

Non, ce sont des modèles différents. Coordinator gère le flux de navigation : décide quel écran afficher, crée les ViewControllers et les connecte. Router est une abstraction sur UINavigationController : push, present, pop, dismiss. Coordinator utilise Router pour effectuer la navigation. Dans certaines implémentations, Router inclut la logique de Coordinator (Router-per-screen), mais cela s'écarte du modèle original.

Comment éviter les retain cycles dans Coordinator ?

Deux sources principales de fuites : childCoordinators (le parent retient l'enfant, oubliant de le supprimer) et Router (UINavigationController retient ViewController). Solution : toujours supprimer le Coordinator enfant du tableau lors de finish(). Utiliser une référence weak pour le délégué. Pour Router — ne pas conserver de référence forte sur UINavigationController s'il est déjà dans la hiérarchie de la fenêtre. Testez le deinit du Coordinator.

Quand Coordinator est-il excessif ?

Pour les applications avec 3 à 5 écrans, Coordinator est excessif — segue ou simple navigationController.pushViewController est plus simple. Pour les applications SwiftUI avec NavigationStack — également redondant. Coordinator se justifie pour les applications avec 15+ écrans, les flux complexes (onboarding avec branchements, autorisation avec récupération de mot de passe) et les projets mixtes UIKit/SwiftUI.

Comment tester Coordinator ?

Mock Router — vérifier quelles méthodes sont appelées et avec quels paramètres. Vérifier childCoordinators : après start() le tableau n'est pas vide, après finish() — vide. Coordinator est testé sans UI : Router est un protocole, son mock ne nécessite pas UIKit. Utilisez XCTestExpectation pour les flux asynchrones. Sous Android — test similaire de NavigationController et NavHost avec navigation mock.

Résumé

  • Coordinator — modèle de navigation qui extrait les transitions du ViewController vers une classe séparée
  • Router — abstraction de UINavigationController utilisée par Coordinator
  • Hiérarchie — Coordinateurs parents et enfants avec délégation des résultats
  • MVVM-C — approche standard pour les projets UIKit avec Coordinator
  • SwiftUI — navigation intégrée via NavigationStack remplace Coordinator
  • Tests — Coordinator est testé via un mock Router sans UIKit

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi