VIPER : concepts clés, le modèle View-Interactor-Presenter-Entity-Router

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

VIPER (View-Interactor-Presenter-Entity-Router) est une architecture modulaire développée chez Mutual Mobile pour les applications iOS. VIPER divise l'application en cinq couches : View gère l'affichage, Interactor la logique métier, Presenter la préparation des données, Entity les modèles de données, Router la navigation entre les modules. VIPER est l'implémentation la plus détaillée du principe de responsabilité unique parmi les architectures mobiles. En savoir plus dans l'article sur objc.io.

Points clés

  • VIPER — cinq composants : View, Interactor, Presenter, Entity, Router avec des limites de responsabilité claires
  • Modularité — chaque écran (module) est isolé, communication via des protocoles
  • Router — sort la navigation de Presenter, résolvant le problème de navigation iOS
  • Interactor — contient la logique métier et ne dépend pas d'UIKit, testé avec des tests unitaires
  • iOS-natif — VIPER a été créé pour UIKit avant SwiftUI et reste la norme pour les grands projets iOS

Qu'est-ce que VIPER : cinq composants de l'architecture modulaire

VIPER (View-Interactor-Presenter-Entity-Router) est un modèle architectural développé en 2013–2014 chez Mutual Mobile pour les grands projets iOS. Chaque écran d'application est un module séparé de cinq composants avec des responsabilités strictement définies. VIPER est l'implémentation la plus stricte du principe de responsabilité unique (Single Responsibility Principle) dans le développement mobile : aucun composant ne fait ce qu'un autre peut faire.

View est un composant passif responsable uniquement de l'affichage des données transmises par Presenter. View ne contient pas de logique métier, ne gère pas la navigation, n'effectue pas de requêtes réseau. En iOS — UIViewController avec un ViewProtocol. Interactor est la couche de logique métier qui travaille avec Entity et les services (réseau, BD, GPS). Interactor n'importe pas UIKit. Presenter est le médiateur entre View et Interactor : reçoit les données d'Interactor, les formate pour l'affichage, les transmet à View. Presenter n'importe pas non plus UIKit. Entity — modèles de données (struct, class). Router — gère la navigation : crée des modules, ouvre des écrans, transmet des données entre les modules.

ComposantResponsabilitéDépendances
ViewAffichage, animations, gestesUIKit (View uniquement)
InteractorLogique métier, réseau, BDEntity, services
PresenterFormatage des données, commandes ViewViewProtocol, Interactor
EntityModèles de donnéesAucune
RouterNavigation, création de modulesUIViewController (pour les transitions)

Les relations entre composants sont décrites par des protocoles. ViewProtocol définit les méthodes d'affichage, InteractorProtocol définit les méthodes de logique métier, PresenterProtocol définit les méthodes de gestion d'événements, RouterProtocol définit les méthodes de navigation. Chaque composant communique avec un autre uniquement via un protocole, ce qui permet de remplacer facilement les implémentations et de tester de manière isolée. En moyenne, un module VIPER pour un écran contient 5 protocoles + 5 classes + 1 Builder/Assembler = 11 fichiers par écran.

VIPER en Swift : Module, Router et Presenter

La construction d'un module VIPER est effectuée dans Builder (ou Assembler), qui crée les cinq composants et les connecte via des protocoles. Builder est le seul endroit où les composants connaissent les types concrets des autres. Après l'assemblage, View est renvoyée vers l'extérieur pour l'affichage, le reste de la chaîne est propre et testé de manière isolée.

swift
// Protocole — View
protocol UserViewProtocol: AnyObject {
    func display(name: String)
    func display(email: String)
    func showLoading()
    func hideLoading()
}

// Protocole — Interactor
protocol UserInteractorProtocol {
    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}

// Protocole — Router
protocol UserRouterProtocol {
    func navigateToProfile(userId: Int)
}

// Interactor — logique métier
final class UserInteractor: UserInteractorProtocol {
    private let service: UserService

    init(service: UserService) { self.service = service }

    func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
        service.fetchUser(id: id, completion: completion)
    }
}

// Presenter — préparation des données
final class UserPresenter {
    private weak var view: UserViewProtocol?
    private let interactor: UserInteractorProtocol
    private let router: UserRouterProtocol

    init(interactor: UserInteractorProtocol, router: UserRouterProtocol) {
        self.interactor = interactor
        self.router = router
    }

    func setView(_ view: UserViewProtocol) {
        self.view = view
    }

    func viewDidLoad() {
        view?.showLoading()
        interactor.fetchUser(id: 42) { [weak self] result in
            guard let self else { return }
            self.view?.hideLoading()
            switch result {
            case .success(let user):
                self.view?.display(name: user.name)
                self.view?.display(email: user.email)
            case .failure(let error):
                // gestion d'erreur
            }
        }
    }
}

// Router — navigation
final class UserRouter: UserRouterProtocol {
    private weak var viewController: UIViewController?

    func setViewController(_ vc: UIViewController) {
        viewController = vc
    }

    func navigateToProfile(userId: Int) {
        let profileModule = ProfileModuleBuilder.build(userId: userId)
        viewController?.navigationController?.pushViewController(profileModule, animated: true)
    }
}

// Builder — assemblage du module
enum UserModuleBuilder {
    static func build(userId: Int) -> UIViewController {
        let service = UserService()
        let interactor = UserInteractor(service: service)
        let router = UserRouter()
        let presenter = UserPresenter(interactor: interactor, router: router)
        let viewController = UserViewController(presenter: presenter)
        presenter.setView(viewController)
        router.setViewController(viewController)
        return viewController
    }
}

Builder/Assembler est un élément clé de VIPER, implémentant l'injection de dépendances manuellement. L'injection de dépendances via le constructeur (constructor injection) garantit qu'un composant ne peut pas être créé sans ses dépendances. Dans VIPER moderne, Builder peut utiliser Swinject (conteneur DI), mais l'assemblage manuel reste plus transparent pour les tests. Chez IT Sectr, nous appliquons VIPER avec assemblage manuel pour les modules à logique complexe — cela simplifie la lecture du code pour les nouveaux développeurs.

Mapper (Formatter) est un sixième composant optionnel de VIPER. Mapper transforme Entity (modèles BD/serveur) en ViewModel (modèles d'affichage). Entity contient UserDTO avec les champs id, first_name, last_name, email. ViewModel — UserDisplayItem avec name (first_name + last_name) et email. Mapper est exécuté dans Presenter. Si le mappage de données est complexe (plusieurs Entity → un ViewModel), Mapper est extrait dans une classe séparée pour les tests.

Communication entre les modules VIPER

Les modules VIPER sont isolés et ne se connaissent pas. La communication entre les modules se fait via Router. Lorsqu'un utilisateur clique sur le bouton "Profil" sur l'écran utilisateur, Presenter appelle router.navigateToProfile(userId: 42). Router crée un nouveau module via ProfileModuleBuilder.build(userId: 42) et l'ouvre via navigationController.push. Flux de données : Module A → Router A → Builder du Module B → Le Module B est créé et ouvert.

Le renvoi de données (par exemple, sélectionner une ville sur l'écran de sélection → revenir à l'écran d'édition du profil) dans VIPER est implémenté via des délégués ou des closures. Le Module B définit le protocole ModuleBDelegate avec la méthode didSelectCity(_ city: City). Le Module A implémente ce protocole. Router A transmet le délégué au Builder du Module B. Lorsqu'une ville est sélectionnée, le Module B appelle delegate?.didSelectCity(city). C'est une pratique standard iOS, familière à tout développeur UIKit.

ScénarioMécanismeExemple
Navigation avantRouter → BuildernavigateToProfile(userId:)
Renvoi de donnéesDelegatedidSelectCity(_:)
Notification systèmeNotificationCenterUserDidLogout
Événement d'InteractorPresenter → ViewMessage WebSocket

NotificationCenter est utilisé pour les événements système (déconnexion, changement de forfait, notifications push) qui affectent plusieurs modules simultanément. Router ou AppDelegate s'abonne à Notification, crée le module requis ou met à jour l'état. VIPER n'interdit pas NotificationCenter — il est important qu'il soit utilisé uniquement pour les événements 1-à-plusieurs, tandis que pour la communication 1-à-1, on utilise des délégués ou des closures.

Comparaison de VIPER avec MVVM et Clean Architecture

VIPER vs MVVM — VIPER nécessite 2 à 3 fois plus de code par écran mais fournit un isolement absolu des composants. MVVM avec ViewModel + SwiftUI est plus simple et plus rapide, mais s'adapte moins bien aux équipes de 5+ développeurs. VIPER définit strictement qui est responsable de quoi : Interactor — uniquement la logique métier, Presenter — le formatage, Router — la navigation. Dans MVVM, ViewModel grossit souvent, prenant en charge la navigation et la logique métier.

VIPER vs Clean Architecture — VIPER est un cas spécifique de Clean Architecture adapté pour iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller dans les termes de Robert Martin. Clean Architecture ajoute une couche Gateway/Repository entre Interactor et les données, qui n'est généralement pas séparée dans VIPER. Pour les projets modernes en SwiftUI, la plupart des équipes choisissent Clean Architecture (The Composable Architecture) ou MVVM, laissant VIPER pour les héritages UIKit.

Quand choisir VIPER — équipes de 5+ développeurs, projets UIKit de 50+ écrans, exigences de test supérieures à 80%, iOS uniquement (VIPER n'est pas portable sur Android sans réécriture). VIPER fournit une structure prévisible : un nouveau développeur comprend un module en 15 minutes. Cependant, la vitesse de développement est 20 à 30% inférieure par rapport à MVVM en raison du plus grand nombre de fichiers. Chez IT Sectr, nous utilisons VIPER pour les projets d'entreprise UIKit avec des équipes de 3+ personnes et préférons Clean Architecture pour les nouveaux projets SwiftUI.

Test des modules VIPER

VIPER est conçu pour les tests — chaque composant est testé isolément via des protocoles. Interactor est testé avec des services mock : on vérifie que fetchUser est appelé avec le bon ID et que le résultat est transmis à Presenter. Presenter est testé avec des objets mock de View et Interactor. Router est testé avec une navigation mock : on vérifie que navigateToProfile est appelé avec le bon userId et que le bon module est créé. View est testé avec des tests UI (XCUITest).

swift
import XCTest

final class UserPresenterTests: XCTestCase {
    func testViewDidLoad_callsFetchUserAndUpdatesView() {
        // Given
        let view = MockUserView()
        let interactor = MockUserInteractor()
        let router = MockUserRouter()
        let presenter = UserPresenter(interactor: interactor, router: router)
        presenter.setView(view)
        let expectedUser = User(id: 42, name: "John", email: "john@test.com")
        interactor.result = .success(expectedUser)

        // When
        presenter.viewDidLoad()

        // Then
        XCTAssertEqual(interactor.capturedUserId, 42)
        XCTAssertEqual(view.displayedName, "John")
        XCTAssertTrue(view.didShowLoading)
    }
}

Les objets mock pour VIPER sont créés manuellement (classe avec propriétés de capture stockées) ou via des bibliothèques comme Cuckoo / Mockingbird. Les classes mock manuelles sont plus simples et plus claires, surtout pour former les nouveaux développeurs. Chaque mock stocke des valeurs capturées (capturedUserId, displayedName) et des indicateurs d'appel (didShowLoading). À la fin du test, on vérifie non seulement que la méthode a été appelée, mais aussi avec quels paramètres — cela donne confiance dans la justesse du flux de données.

Couverture de code dans les projets VIPER d'IT Sectr atteint 85–95% pour Interactor, 90–95% pour Presenter, 70–80% pour Router, 30–50% pour View (via des tests UI). View est testé avec des tests de capture d'écran (SnapshotTesting, 1,5K étoiles) — c'est plus rapide que XCUITest et couvre plus de cas. La couverture globale d'un projet VIPER est généralement de 70–80%, ce qui est plus élevé qu'un projet MVVM (50–65%), mais nécessite plus de temps pour écrire les tests (30–40% du temps de développement contre 20–25% dans MVVM).

Questions fréquentes

Combien de fichiers dans un module VIPER ?

Au minimum 11 fichiers : 5 protocoles (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implémentations (ViewController, Interactor, Presenter, Router, Entity) et Builder/Assembler. Avec Mapper (Formatter) — 12–13. Pour un projet de 50 écrans, cela représente 550–650 fichiers rien que pour les modules VIPER. MVVM nécessite 3 fichiers par écran (ViewModel, View, Model) — 150 fichiers pour 50 écrans.

Peut-on utiliser VIPER sur Android ?

Oui, théoriquement VIPER est portable sur Android, mais en pratique il n'est pas utilisé — Google recommande MVVM avec Jetpack. VIPER a été créé pour iOS UIKit, où ViewController est difficile à tester en raison de son cycle de vie. Sur Android, Jetpack ViewModel résout le problème de test sans isolement VIPER. L'équivalent Android de VIPER est Clean Architecture avec une répartition en modules/features.

Quelle est la différence entre VIPER et Clean Architecture ?

VIPER est une implémentation spécifique à iOS de Clean Architecture. Interactor correspond à Use Case, Entity — Domain Model, Presenter — couche Presentation. Clean Architecture ajoute Repository/Gateway entre Interactor et les données, qui dans VIPER sont généralement implémentés à l'intérieur d'Interactor. Clean Architecture ne prescrit pas de Router — la navigation est laissée à l'implémentation.

A-t-on besoin de VIPER pour les projets SwiftUI ?

Non — SwiftUI est conçu pour MVVM + Combine. VIPER dans SwiftUI est redondant : cinq composants par écran avec une UI déclarative est un surcoût sans avantage. Pour SwiftUI, choisissez MVVM ou TCA (The Composable Architecture). VIPER reste pertinent pour les héritages UIKit et les projets où iOS 12 et inférieur est la version minimale.

Comment transmettre des données entre modules VIPER ?

Via Router. Le Module A appelle router.navigateToProfile(userId: id). Router A crée le module B via Builder, transmet userId. La communication de retour — via un délégué : le module B définit le protocole ModuleBDelegate, le module A l'implémente et le transmet via Router. Les événements système (déconnexion) — via NotificationCenter.

Résumé

  • VIPER — cinq composants avec séparation stricte : View, Interactor, Presenter, Entity, Router
  • Modularité — chaque écran est isolé, Builder assemble les dépendances via constructor injection
  • Router — sort la navigation de Presenter, résolvant le problème de navigation sur iOS
  • Interactor — logique métier propre sans UIKit, testé avec des tests unitaires
  • Volume de code — 11+ fichiers par écran, développement 20–30% plus lent que MVVM
  • Tests — couverture 70–80%, Interactor et Presenter testés via des objets mock
  • SwiftUI vs UIKit — VIPER pour UIKit (héritage), MVVM/TCA pour SwiftUI

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