VIPER (View-Interactor-Presenter-Entity-Router) ist eine modulare Architektur, die bei Mutual Mobile für iOS-Anwendungen entwickelt wurde. VIPER teilt die Anwendung in fünf Schichten auf: View ist für die Anzeige zuständig, Interactor für die Geschäftslogik, Presenter für die Datenaufbereitung, Entity für die Datenmodelle und Router für die Navigation zwischen Modulen. VIPER ist die detaillierteste Umsetzung des Prinzips der einzigen Verantwortung unter den mobilen Architekturen. Lesen Sie mehr im Artikel auf objc.io.
Wichtige Erkenntnisse
VIPER (View-Interactor-Presenter-Entity-Router) ist ein Architekturmuster, das 2013–2014 bei Mutual Mobile für große iOS-Projekte entwickelt wurde. Jeder Anwendungsbildschirm ist ein separates Modul aus fünf Komponenten mit streng definierten Verantwortlichkeiten. VIPER ist die strengste Umsetzung des Prinzips der einzigen Verantwortung (Single Responsibility Principle) in der mobilen Entwicklung: Keine Komponente tut, was eine andere tun kann.
View ist eine passive Komponente, die nur für die Anzeige der vom Presenter übergebenen Daten verantwortlich ist. View enthält keine Geschäftslogik, verarbeitet keine Navigation und führt keine Netzwerkanfragen durch. In iOS — UIViewController mit einem ViewProtocol. Interactor ist die Geschäftslogikschicht, die mit Entity und Diensten (Netzwerk, DB, GPS) arbeitet. Interactor importiert UIKit nicht. Presenter ist der Vermittler zwischen View und Interactor: Er erhält Daten vom Interactor, formatiert sie zur Anzeige und übergibt sie an View. Presenter importiert UIKit ebenfalls nicht. Entity — Datenmodelle (struct, class). Router — verwaltet die Navigation: erstellt Module, öffnet Bildschirme, übergibt Daten zwischen Modulen.
| Komponente | Verantwortung | Abhängigkeiten |
|---|---|---|
| View | Anzeige, Animationen, Gesten | UIKit (nur View) |
| Interactor | Geschäftslogik, Netzwerk, DB | Entity, Dienste |
| Presenter | Datenformatierung, View-Befehle | ViewProtocol, Interactor |
| Entity | Datenmodelle | Keine |
| Router | Navigation, Modulerstellung | UIViewController (für Übergänge) |
Beziehungen zwischen Komponenten werden durch Protokolle beschrieben. ViewProtocol definiert Anzeigemethoden, InteractorProtocol definiert Methoden der Geschäftslogik, PresenterProtocol definiert Ereignisbehandlungsmethoden, RouterProtocol definiert Navigationsmethoden. Jede Komponente kommuniziert nur über ein Protokoll mit einer anderen, was einen einfachen Austausch von Implementierungen und isolierte Tests ermöglicht. Im Durchschnitt enthält ein VIPER-Modul für einen Bildschirm 5 Protokolle + 5 Klassen + 1 Builder/Assembler = 11 Dateien pro Bildschirm.
Der Aufbau eines VIPER-Moduls erfolgt im Builder (oder Assembler), der alle fünf Komponenten erstellt und über Protokolle verbindet. Builder ist der einzige Ort, an dem die Komponenten die konkreten Typen der anderen kennen. Nach der Montage wird View zur Anzeige nach außen zurückgegeben, der Rest der Kette ist sauber und wird isoliert getestet.
// Protokoll — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// Protokoll — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// Protokoll — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — Geschäftslogik
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 — Datenaufbereitung
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):
// Fehlerbehandlung
}
}
}
}
// 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 — Modulaufbau
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 ist ein Schlüsselelement von VIPER, das Dependency Injection manuell implementiert. Die Abhängigkeitsinjektion über den Konstruktor (constructor injection) stellt sicher, dass eine Komponente nicht ohne ihre Abhängigkeiten erstellt werden kann. Im modernen VIPER kann Builder Swinject (DI-Container) verwenden, aber die manuelle Montage bleibt für Tests transparenter. Bei IT Sectr wenden wir VIPER mit manueller Montage für Module mit komplexer Logik an — dies vereinfacht das Lesen des Codes für neue Entwickler.
Mapper (Formatter) ist eine optionale sechste Komponente von VIPER. Mapper wandelt Entity (DB-/Server-Modelle) in ViewModel (Anzeigemodelle) um. Entity enthält UserDTO mit den Feldern id, first_name, last_name, email. ViewModel — UserDisplayItem mit name (first_name + last_name) und email. Mapper wird im Presenter ausgeführt. Wenn das Daten-Mapping komplex ist (mehrere Entity → ein ViewModel), wird Mapper zum Testen in eine separate Klasse ausgelagert.
VIPER-Module sind isoliert und wissen nichts voneinander. Die Kommunikation zwischen Modulen erfolgt über Router. Wenn ein Benutzer auf dem Benutzerbildschirm auf die Schaltfläche "Profil" klickt, ruft Presenter router.navigateToProfile(userId: 42) auf. Router erstellt ein neues Modul über ProfileModuleBuilder.build(userId: 42) und öffnet es über navigationController.push. Datenfluss: Modul A → Router A → Modul B Builder → Modul B wird erstellt und geöffnet.
Datenrückgabe (z. B. Auswählen einer Stadt auf dem Auswahlbildschirm → Rückkehr zum Profilbearbeitungsbildschirm) wird in VIPER über Delegaten oder Closures implementiert. Modul B definiert das ModuleBDelegate-Protokoll mit der Methode didSelectCity(_ city: City). Modul A implementiert dieses Protokoll. Router A übergibt den Delegaten an Modul B Builder. Wenn eine Stadt ausgewählt wird, ruft Modul B delegate?.didSelectCity(city) auf. Dies ist eine Standard-iOS-Praxis, die jedem UIKit-Entwickler vertraut ist.
| Szenario | Mechanismus | Beispiel |
|---|---|---|
| Vorwärtsnavigation | Router → Builder | navigateToProfile(userId:) |
| Datenrückgabe | Delegate | didSelectCity(_:) |
| Systembenachrichtigung | NotificationCenter | UserDidLogout |
| Ereignis von Interactor | Presenter → View | WebSocket-Nachricht |
NotificationCenter wird für Systemereignisse (Abmeldung, Tarifwechsel, Push-Benachrichtigungen) verwendet, die mehrere Module gleichzeitig betreffen. Router oder AppDelegate abonniert Notification, erstellt das erforderliche Modul oder aktualisiert den Status. VIPER verbietet NotificationCenter nicht — wichtig ist, dass es nur für 1-zu-viele-Ereignisse verwendet wird, während für 1-zu-1-Kommunikation Delegaten oder Closures verwendet werden.
VIPER vs MVVM — VIPER benötigt 2–3 Mal mehr Code pro Bildschirm, bietet aber absolute Komponentenisolierung. MVVM mit ViewModel + SwiftUI ist einfacher und schneller, skaliert aber schlechter für Teams mit 5+ Entwicklern. VIPER definiert streng, wer für was verantwortlich ist: Interactor — nur Geschäftslogik, Presenter — Formatierung, Router — Navigation. Bei MVVM wächst ViewModel oft und übernimmt Navigation und Geschäftslogik.
VIPER vs Clean Architecture — VIPER ist ein spezifischer Fall von Clean Architecture, der für iOS UIKit adaptiert wurde. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller in den Begriffen von Robert Martin. Clean Architecture fügt eine Gateway/Repository-Schicht zwischen Interactor und Daten hinzu, die in VIPER normalerweise nicht getrennt wird. Für moderne SwiftUI-Projekte wählen die meisten Teams Clean Architecture (The Composable Architecture) oder MVVM und überlassen VIPER UIKit-Legacy.
Wann VIPER wählen — Teams mit 5+ Entwicklern, UIKit-Projekte mit 50+ Bildschirmen, Testanforderungen über 80%, nur iOS (VIPER ist ohne Umschreiben nicht auf Android portierbar). VIPER bietet eine vorhersagbare Struktur: Ein neuer Entwickler versteht ein Modul in 15 Minuten. Allerdings ist die Entwicklungsgeschwindigkeit aufgrund der größeren Anzahl von Dateien um 20–30% niedriger als bei MVVM. Bei IT Sectr verwenden wir VIPER für Enterprise-UIKit-Projekte mit Teams ab 3 Personen und bevorzugen Clean Architecture für neue SwiftUI-Projekte.
VIPER ist für Tests konzipiert — jede Komponente wird isoliert über Protokolle getestet. Interactor wird mit Mock-Diensten getestet: Es wird geprüft, ob fetchUser mit der richtigen ID aufgerufen wurde und das Ergebnis an Presenter übergeben wurde. Presenter wird mit Mock-Objekten von View und Interactor getestet. Router wird mit Mock-Navigation getestet: Es wird geprüft, ob navigateToProfile mit der richtigen userId aufgerufen und das richtige Modul erstellt wurde. View wird mit UI-Tests (XCUITest) getestet.
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-Objekte für VIPER werden manuell erstellt (Klasse mit gespeicherten Capture-Eigenschaften) oder über Bibliotheken wie Cuckoo / Mockingbird. Manuelle Mock-Klassen sind einfacher und klarer, besonders für die Schulung neuer Entwickler. Jeder Mock speichert erfasste Werte (capturedUserId, displayedName) und Aufrufflags (didShowLoading). Am Ende des Tests wird nicht nur geprüft, ob die Methode aufgerufen wurde, sondern auch mit welchen Parametern — dies gibt Vertrauen in die Korrektheit des Datenflusses.
Codeabdeckung in IT Sectr VIPER-Projekten erreicht 85–95% für Interactor, 90–95% für Presenter, 70–80% für Router, 30–50% für View (über UI-Tests). View wird mit Snapshot-Tests getestet (SnapshotTesting, 1,5K Sterne) — dies ist schneller als XCUITest und deckt mehr Fälle ab. Die Gesamtabdeckung eines VIPER-Projekts beträgt normalerweise 70–80%, was höher ist als bei einem MVVM-Projekt (50–65%), erfordert aber mehr Zeit zum Schreiben von Tests (30–40% der Entwicklungszeit gegenüber 20–25% bei MVVM).
Häufig gestellte Fragen
Mindestens 11 Dateien: 5 Protokolle (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 Implementierungen (ViewController, Interactor, Presenter, Router, Entity) und Builder/Assembler. Mit Mapper (Formatter) — 12–13. Für ein Projekt mit 50 Bildschirmen sind das 550–650 Dateien allein für VIPER-Module. MVVM benötigt 3 Dateien pro Bildschirm (ViewModel, View, Model) — 150 Dateien für 50 Bildschirme.
Ja, theoretisch ist VIPER auf Android portierbar, wird aber in der Praxis nicht verwendet — Google empfiehlt MVVM mit Jetpack. VIPER wurde für iOS UIKit entwickelt, wo ViewController aufgrund seines Lebenszyklus schwer zu testen ist. Auf Android löst Jetpack ViewModel das Testproblem ohne VIPER-Isolierung. Das Android-Äquivalent von VIPER ist Clean Architecture mit Modul-/Feature-Aufteilung.
VIPER ist eine iOS-spezifische Implementierung von Clean Architecture. Interactor entspricht Use Case, Entity — Domain Model, Presenter — Presentation-Schicht. Clean Architecture fügt ein Repository/Gateway zwischen Interactor und Daten hinzu, das in VIPER normalerweise innerhalb des Interactors implementiert wird. Clean Architecture schreibt keinen Router vor — die Navigation bleibt der Implementierung überlassen.
Nein — SwiftUI ist für MVVM + Combine konzipiert. VIPER in SwiftUI ist überflüssig: Fünf Komponenten pro Bildschirm bei deklarativer UI sind Overhead ohne Nutzen. Für SwiftUI wählen Sie MVVM oder TCA (The Composable Architecture). VIPER bleibt für UIKit-Legacy und Projekte relevant, bei denen iOS 12 und darunter die Mindestversion ist.
Über Router. Modul A ruft router.navigateToProfile(userId: id) auf. Router A erstellt Modul B über Builder und übergibt userId. Rückwärtige Kommunikation — über einen Delegaten: Modul B definiert das ModuleBDelegate-Protokoll, Modul A implementiert es und übergibt es über Router. Systemereignisse (Abmeldung) — über NotificationCenter.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch