VIPER: kernconcepten, het View-Interactor-Presenter-Entity-Router patroon

Auteur: IT Sectr Gepubliceerd: 2026-02-16 Leestijd: 9 min

VIPER (View-Interactor-Presenter-Entity-Router) — modulaire architectuur ontwikkeld bij Mutual Mobile voor iOS-applicaties. VIPER verdeelt de applicatie in vijf lagen: View is verantwoordelijk voor weergave, Interactor — voor bedrijfslogica, Presenter — voor gegevensvoorbereiding, Entity — voor gegevensmodellen, Router — voor navigatie tussen modules. VIPER is de meest gedetailleerde implementatie van het single responsibility-principe onder mobiele architecturen. Meer — in het artikel op objc.io.

Belangrijkste punten

  • VIPER — vijf componenten: View, Interactor, Presenter, Entity, Router met duidelijke verantwoordelijkheidsgrenzen
  • Modulariteit — elk scherm (module) is geïsoleerd, communicatie via protocollen
  • Router — haalt navigatie uit Presenter, lost het iOS-navigatieprobleem op
  • Interactor — bevat bedrijfslogica en is niet afhankelijk van UIKit, wordt getest met unittesten
  • iOS-native — VIPER is gemaakt voor UIKit vóór de komst van SwiftUI en blijft de standaard voor grote iOS-projecten

Wat is VIPER: vijf componenten van modulaire architectuur

VIPER (View-Interactor-Presenter-Entity-Router) — een architectuurpatroon ontwikkeld in 2013–2014 bij Mutual Mobile voor grote iOS-projecten. Elk scherm van de applicatie is een aparte module van vijf componenten met strikt gedefinieerde verantwoordelijkheden. VIPER is de meest strikte implementatie van het Single Responsibility Principle in mobiele ontwikkeling: geen enkel component doet wat een ander kan doen.

View — passief component dat alleen verantwoordelijk is voor het weergeven van gegevens die door Presenter worden doorgegeven. View bevat geen bedrijfslogica, verwerkt geen navigatie, doet geen netwerkverzoeken. In iOS — UIViewController met het ViewProtocol. Interactor — de laag bedrijfslogica die werkt met Entity en services (netwerk, database, GPS). Interactor importeert UIKit niet. Presenter — tussenpersoon tussen View en Interactor: ontvangt gegevens van Interactor, formatteert ze voor weergave, geeft ze door aan View. Presenter importeert ook UIKit niet. Entity — gegevensmodellen (struct, class). Router — beheert navigatie: maakt modules, opent schermen, geeft gegevens door tussen modules.

ComponentVerantwoordelijkheidAfhankelijkheden
ViewWeergave, animaties, gebarenUIKit (alleen View)
InteractorBedrijfslogica, netwerk, databaseEntity, services
PresenterGegevensformattering, View-commando'sViewProtocol, Interactor
EntityGegevensmodellenGeen
RouterNavigatie, modules makenUIViewController (voor overgangen)

Verbindingen tussen componenten worden beschreven door protocollen. ViewProtocol definieert weergavemethoden, InteractorProtocol — methoden voor bedrijfslogica, PresenterProtocol — methoden voor gebeurtenisafhandeling, RouterProtocol — navigatiemethoden. Elk component communiceert met een ander alleen via het protocol, wat eenvoudige vervanging van implementaties en geïsoleerd testen mogelijk maakt. Een gemiddelde VIPER-module van één scherm bevat 5 protocollen + 5 klassen + 1 Builder/Assembler = 11 bestanden per scherm.

VIPER in Swift: module, Router en Presenter

Bouw van de VIPER-module wordt uitgevoerd in Builder (of Assembler), die alle vijf componenten maakt en ze via protocollen verbindt. Builder is de enige plek waar componenten elkaars concrete types kennen. Na de bouw wordt View naar buiten teruggegeven voor weergave, de rest van de keten is schoon en wordt geïsoleerd getest.

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

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

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

// Interactor — bedrijfslogica
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 — gegevensvoorbereiding
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):
                // foutafhandeling
            }
        }
    }
}

// Router — navigatie
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 — modulebouw
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 — het sleutelelement van VIPER dat handmatige Dependency Injection implementeert. Constructor injectie garandeert dat een component niet kan worden gemaakt zonder zijn afhankelijkheden. In moderne VIPER kan Builder Swinject (DI-container) gebruiken, maar handmatige bouw blijft transparanter voor testen. Bij IT Sectr passen we VIPER met handmatige bouw toe voor modules met complexe logica — dit vereenvoudigt het lezen van code voor nieuwe ontwikkelaars.

Mapper (Formatter) — een optioneel zesde component van VIPER. Mapper zet Entity (database/server-modellen) om naar ViewModel (weergavemodellen). Entity bevat UserDTO met velden id, first_name, last_name, email. ViewModel — UserDisplayItem met name (first_name + last_name) en email. Mapper wordt uitgevoerd in Presenter. Als gegevensmapping complex is (meerdere Entity → één ViewModel), wordt Mapper in een aparte klasse ondergebracht voor testen.

Communicatie tussen VIPER-modules

VIPER-modules zijn geïsoleerd en weten niet van elkaar. Communicatie tussen modules vindt plaats via Router. Wanneer de gebruiker op de knop „Profiel" op het gebruikersscherm klikt, roept Presenter router.navigateToProfile(userId: 42) aan. Router maakt een nieuwe module via ProfileModuleBuilder.build(userId: 42) en opent deze via navigationController.push. Gegevensstroom: Module A → Router A → Module B Builder → Module B wordt gemaakt en geopend.

Gegevens teruggeven (bijvoorbeeld een stad geselecteerd op het selectiescherm → teruggegeven aan het profielbewerkingsscherm) wordt in VIPER geïmplementeerd via delegaten of closures. Module B definieert het protocol ModuleBDelegate met de methode didSelectCity(_ city: City). Module A implementeert dit protocol. Router A geeft de delegaat door aan Module B Builder. Bij selectie van de stad roept Module B delegate?.didSelectCity(city) aan. Dit is een standaard iOS-praktijk, bekend bij elke UIKit-ontwikkelaar.

ScenarioMechanismeVoorbeeld
Voorwaartse overgangRouter → BuildernavigateToProfile(userId:)
Gegevens teruggevenDelegatedidSelectCity(_:)
SysteemmeldingNotificationCenterUserDidLogout
Gebeurtenis uit InteractorPresenter → ViewWebSocket message

NotificationCenter wordt gebruikt voor systeemgebeurtenissen (uitloggen, tariefwijziging, pushmeldingen) die meerdere modules tegelijk beïnvloeden. Router of AppDelegate abonneert zich op Notification, maakt de benodigde module aan of werkt de status bij. VIPER verbiedt NotificationCenter niet — belangrijk is dat het alleen wordt gebruikt voor 1-op-veel gebeurtenissen, en voor 1-op-1 communicatie worden delegaten of closures toegepast.

Vergelijking van VIPER met MVVM en Clean Architecture

VIPER vs MVVM — VIPER vereist 2–3 keer meer code per scherm, maar biedt absolute isolatie van componenten. MVVM met ViewModel + SwiftUI is eenvoudiger en sneller, maar schaalt minder goed in teams van 5+ ontwikkelaars. VIPER bepaalt strikt wie waarvoor verantwoordelijk is: Interactor — alleen bedrijfslogica, Presenter — formattering, Router — navigatie. In MVVM groeit ViewModel vaak, waarbij het navigatie en bedrijfslogica overneemt.

VIPER vs Clean Architecture — VIPER is een specifiek geval van Clean Architecture, aangepast voor iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller in de termen van Robert Martin. Clean Architecture voegt een Gateway/Repository-laag toe tussen Interactor en gegevens, die in VIPER meestal niet wordt gescheiden. Voor moderne projecten in SwiftUI kiezen de meeste teams voor Clean Architecture (The Composable Architecture) of MVVM, en laten VIPER voor legacy op UIKit.

Wanneer VIPER kiezen — teams vanaf 5 ontwikkelaars, project op UIKit vanaf 50 schermen, testvereisten boven 80%, alleen iOS (VIPER wordt niet overgezet naar Android zonder herschrijven). VIPER biedt een voorspelbare structuur: een nieuwe ontwikkelaar begrijpt de module binnen 15 minuten. De ontwikkelsnelheid is echter 20–30% lager in vergelijking met MVVM vanwege het grotere aantal bestanden. Bij IT Sectr gebruiken we VIPER voor enterprise-projecten op UIKit met teams vanaf 3 personen en geven we de voorkeur aan Clean Architecture voor nieuwe projecten in SwiftUI.

Testen van VIPER-modules

VIPER is ontworpen voor testen — elk component wordt geïsoleerd getest via protocollen. Interactor wordt getest met mock-services: er wordt gecontroleerd of fetchUser met de juiste ID is aangeroepen en of het resultaat aan Presenter is doorgegeven. Presenter wordt getest met mock-objecten van View en Interactor. Router wordt getest met mock-navigatie: er wordt gecontroleerd of navigateToProfile met de juiste userId is aangeroepen en of de juiste module is gemaakt. View wordt getest met UI-tests (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)
    }
}

Mock-objecten voor VIPER worden handmatig gemaakt (klasse met opgeslagen capture-eigenschappen) of via de bibliotheken Cuckoo / Mockingbird. Handmatige mock-klassen zijn eenvoudiger en begrijpelijker, vooral voor het trainen van nieuwe ontwikkelaars. Elke mock slaat vastgelegde waarden op (capturedUserId, displayedName) en aanroeptags (didShowLoading). Aan het einde van de test wordt niet alleen gecontroleerd of de methode is aangeroepen, maar ook met welke parameters — dit geeft vertrouwen in de juistheid van de gegevensstroom.

Code dekking in VIPER-projecten van IT Sectr bereikt 85–95% voor Interactor, 90–95% voor Presenter, 70–80% voor Router, 30–50% voor View (via UI-tests). View wordt getest met schermafbeeldingtests (SnapshotTesting, 1,5K sterren) — dit is sneller dan XCUITest en dekt meer gevallen. De totale dekkingsgraad van een VIPER-project is meestal 70–80%, wat hoger is dan een MVVM-project (50–65%), maar vereist meer tijd voor het schrijven van tests (30–40% van de ontwikkeltijd versus 20–25% in MVVM).

Veelgestelde vragen

Hoeveel bestanden heeft één VIPER-module?

Minimaal 11 bestanden: 5 protocollen (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementaties (ViewController, Interactor, Presenter, Router, Entity) en Builder/Assembler. Met Mapper (Formatter) — 12–13. Voor een project met 50 schermen is dit 550–650 bestanden alleen voor VIPER-modules. MVVM vereist 3 bestanden per scherm (ViewModel, View, Model) — 150 bestanden voor 50 schermen.

Kan VIPER op Android worden gebruikt?

Ja, theoretisch is VIPER overdraagbaar naar Android, maar in de praktijk wordt het niet toegepast — Google beveelt MVVM met Jetpack aan. VIPER is gemaakt voor iOS UIKit, waar ViewController moeilijk te testen is vanwege de levenscyclus. Op Android lost Jetpack ViewModel het testprobleem op zonder VIPER-isolatie. De Android-analoog van VIPER — Clean Architecture met opsplitsing in module/feature.

Wat is het verschil tussen VIPER en Clean Architecture?

VIPER is een iOS-specifieke implementatie van Clean Architecture. Interactor komt overeen met Use Case, Entity — Domain Model, Presenter — Presentation-laag. Clean Architecture voegt een Repository/Gateway-laag toe tussen Interactor en gegevens, die in VIPER meestal binnen Interactor is geïmplementeerd. Clean Architecture schrijft geen Router voor — navigatie wordt overgelaten aan de implementatie.

Is VIPER nodig voor SwiftUI-projecten?

Nee — SwiftUI is ontworpen voor MVVM + Combine. VIPER in SwiftUI is overbodig: vijf componenten voor één scherm met declaratieve UI is overhead zonder voordeel. Kies voor SwiftUI MVVM of TCA (The Composable Architecture). VIPER blijft relevant voor UIKit-legacy en projecten waar iOS 12 en lager de minimale versie is.

Hoe geef ik gegevens door tussen VIPER-modules?

Via Router. Module A roept router.navigateToProfile(userId: id) aan. Router A maakt module B via Builder, geeft userId door. Teruggeven — via delegatie: module B definieert het protocol ModuleBDelegate, module A implementeert het en geeft het door via Router. Systeemgebeurtenissen (uitloggen) — via NotificationCenter.

Samenvatting

  • VIPER — vijf componenten met strikte scheiding: View, Interactor, Presenter, Entity, Router
  • Modulariteit — elk scherm is geïsoleerd, Builder assembleert afhankelijkheden via constructor injection
  • Router — haalt navigatie uit Presenter, lost het navigatieprobleem op iOS op
  • Interactor — zuivere bedrijfslogica zonder UIKit, getest met unittesten
  • Hoeveelheid code — 11+ bestanden per scherm, ontwikkeling 20–30% langzamer dan MVVM
  • Testen — 70–80% dekking, Interactor en Presenter worden getest via mock-objecten
  • SwiftUI vs UIKit — VIPER voor UIKit (legacy), MVVM/TCA voor SwiftUI

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook