VIPER (View-Interactor-Presenter-Entity-Router) — moduláris architektúra, amelyet a Mutual Mobile fejlesztett iOS alkalmazásokhoz. A VIPER öt rétegre osztja az alkalmazást: a View a megjelenítésért felel, az Interactor — az üzleti logikáért, a Presenter — az adatok előkészítéséért, az Entity — az adatmodellekért, a Router — a modulok közötti navigációért. A VIPER a legrészletesebb megvalósítása az egyetlen felelősség elvének a mobil architektúrák között. Bővebben — a cikkben az objc.io-n.
Főbb pontok
VIPER (View-Interactor-Presenter-Entity-Router) — egy architekturális minta, amelyet 2013–2014-ben a Mutual Mobile fejlesztett nagy iOS projektekhez. Az alkalmazás minden képernyője egy külön modul, amely öt pontosan meghatározott felelősségi körrel rendelkező komponensből áll. A VIPER a szigorúbb megvalósítása az egyszemélyi felelősség elvének (Single Responsibility Principle) a mobilfejlesztésben: egyetlen komponens sem teszi azt, amit egy másik megtehet.
View — passzív komponens, amely csak a Presenter által átadott adatok megjelenítéséért felel. A View nem tartalmaz üzleti logikát, nem kezel navigációt, nem küld hálózati kéréseket. iOS-ben — UIViewController a ViewProtocol protokollal. Interactor — az üzleti logika rétege, amely Entity-vel és szolgáltatásokkal (hálózat, adatbázis, GPS) dolgozik. Az Interactor nem importálja a UIKit-et. Presenter — közvetítő a View és az Interactor között: adatokat kap az Interactortól, formázza a megjelenítéshez, továbbítja a View-nak. A Presenter szintén nem importálja a UIKit-et. Entity — adatmodellek (struct, class). Router — irányítja a navigációt: modulokat hoz létre, képernyőket nyit meg, adatokat továbbít a modulok között.
| Komponens | Felelősség | Függőségek |
|---|---|---|
| View | Megjelenítés, animációk, gesztusok | UIKit (csak View) |
| Interactor | Üzleti logika, hálózat, adatbázis | Entity, szolgáltatások |
| Presenter | Adatok formázása, View parancsok | ViewProtocol, Interactor |
| Entity | Adatmodellek | Nincs |
| Router | Navigáció, modulok létrehozása | UIViewController (átmenetekhez) |
A komponensek közötti kapcsolatokat protokollok írják le. A ViewProtocol meghatározza a megjelenítési metódusokat, az InteractorProtocol — az üzleti logika metódusait, a PresenterProtocol — az eseménykezelés metódusait, a RouterProtocol — a navigációs metódusokat. Minden komponens csak protokollon keresztül kommunikál a másikkal, ami lehetővé teszi a megvalósítások egyszerű cseréjét és az elkülönített tesztelést. Egy átlagos VIPER modul egy képernyőhöz 5 protokollt + 5 osztályt + 1 Builder/Assemblert = 11 fájlt tartalmaz képernyőnként.
A VIPER modul felépítése a Builderben (vagy Assemblerben) történik, amely létrehozza mind az öt komponenst, és protokollokon keresztül kapcsolja össze őket. A Builder az egyetlen hely, ahol a komponensek ismerik egymás konkrét típusait. Az építés után a View kifelé kerül megjelenítésre, a lánc többi része tiszta és elkülönítve tesztelhető.
// 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 — üzleti logika
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 — adatok előkészítése
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):
// hibakezelés
}
}
}
}
// Router — navigáció
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 — modul felépítése
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 — a VIPER kulcseleme, amely manuális függőséginjektálást (Dependency Injection) valósít meg. A konstruktoron keresztüli függőséginjektálás (constructor injection) garantálja, hogy egy komponens nem hozható létre a függőségei nélkül. A modern VIPER-ben a Builder használhat Swinject (DI konténer) keretrendszert, de a manuális felépítés átláthatóbb marad a tesztelés szempontjából. Az IT Sectr-nél manuális felépítéssel alkalmazzuk a VIPER-t az összetett logikájú modulokhoz — ez leegyszerűsíti a kód olvasását az új fejlesztők számára.
Mapper (Formatter) — a VIPER opcionális hatodik komponense. A Mapper az Entity-t (adatbázis/szerver modellek) ViewModel-lé (megjelenítési modellek) alakítja át. Az Entity id, first_name, last_name, email mezőkkel rendelkező UserDTO-t tartalmaz. ViewModel — UserDisplayItem name (first_name + last_name) és email mezőkkel. A Mapper a Presenterben hajtódik végre. Ha az adatleképezés összetett (több Entity → egy ViewModel), a Mapper külön osztályba kerül a tesztelés érdekében.
A VIPER modulok elkülönítettek és nem tudnak egymásról. A modulok közötti kommunikáció a Routern keresztül történik. Amikor a felhasználó megnyomja a „Profil" gombot a felhasználói képernyőn, a Presenter meghívja a router.navigateToProfile(userId: 42) metódust. A Router új modult hoz létre a ProfileModuleBuilder.build(userId: 42) segítségével, és megnyitja a navigationController.push-on keresztül. Adatáramlás: A modul → Router A → B modul Builder → B modul létrejön és megnyílik.
Adatok visszaküldése (például egy város kiválasztása a választóképernyőn → visszaküldés a profil szerkesztőképernyőjére) a VIPER-ben delegáltak vagy closure-ök segítségével valósul meg. A B modul meghatározza a ModuleBDelegate protokollt a didSelectCity(_ city: City) metódussal. Az A modul megvalósítja ezt a protokollt. A Router A továbbítja a delegáltat a Module B Buildernek. A város kiválasztásakor a B modul meghívja a delegate?.didSelectCity(city) metódust. Ez egy szokásos iOS gyakorlat, amely minden UIKit fejlesztő számára ismert.
| Forgatókönyv | Mechanizmus | Példa |
|---|---|---|
| Előre irányuló átmenet | Router → Builder | navigateToProfile(userId:) |
| Adatok visszaküldése | Delegate | didSelectCity(_:) |
| Rendszerértesítés | NotificationCenter | UserDidLogout |
| Esemény az Interactorból | Presenter → View | WebSocket message |
NotificationCenter rendszereseményekhez (kijelentkezés, tarifaváltás, push értesítések) használatos, amelyek egyszerre több modult érintenek. A Router vagy az AppDelegate feliratkozik a Notificationra, létrehozza a szükséges modult vagy frissíti az állapotot. A VIPER nem tiltja a NotificationCenter használatát — fontos, hogy csak 1-többhöz eseményekhez használják, az 1-az-1-hez kommunikációhoz pedig delegáltakat vagy closure-öket alkalmazzanak.
VIPER vs MVVM — a VIPER 2–3-szor több kódot igényel képernyőnként, de abszolút elkülönítést biztosít a komponensek között. Az MVVM ViewModel + SwiftUI segítségével egyszerűbb és gyorsabb, de rosszabbul skálázódik 5+ fejlesztőből álló csapatokban. A VIPER szigorúan meghatározza, ki miért felelős: Interactor — csak üzleti logika, Presenter — formázás, Router — navigáció. Az MVVM-ben a ViewModel gyakran megnő, átvéve a navigációt és az üzleti logikát.
VIPER vs Clean Architecture — a VIPER a Clean Architecture speciális esete, amelyet iOS UIKit-re adaptáltak. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller Robert Martin terminológiájában. A Clean Architecture Gateway/Repository réteget ad az Interactor és az adatok közé, amely a VIPER-ben általában nem kerül elkülönítésre. Modern SwiftUI projektekhez a legtöbb csapat a Clean Architecture-t (The Composable Architecture) vagy az MVVM-et választja, a VIPER-t a UIKit örökölt kódra hagyva.
Mikor válasszuk a VIPER-t — 5 fejlesztőből álló csapatok, 50 képernyőtől induló UIKit projekt, 80% feletti tesztelési követelmények, csak iOS (a VIPER nem vihető át Androidra átírás nélkül). A VIPER kiszámítható struktúrát biztosít: egy új fejlesztő 15 perc alatt megérti a modult. A fejlesztési sebesség azonban 20–30%-kal alacsonyabb az MVVM-hez képest a nagyobb fájlszám miatt. Az IT Sectr-nél a VIPER-t enterprise projektekhez használjuk UIKit-en, 3 fős csapatokkal, és a Clean Architecture-t részesítjük előnyben új SwiftUI projektekhez.
A VIPER tesztelésre tervezték — minden komponens elkülönítve tesztelhető protokollokon keresztül. Az Interactor mock szolgáltatásokkal tesztelhető: ellenőrzik, hogy a fetchUser a megfelelő ID-val lett-e meghívva, és hogy az eredmény továbbításra került-e a Presenternek. A Presenter mock View és Interactor objektumokkal tesztelhető. A Router mock navigációval tesztelhető: ellenőrzik, hogy a navigateToProfile a megfelelő userId-val lett-e meghívva, és hogy a megfelelő modul lett-e létrehozva. A View UI tesztekkel (XCUITest) tesztelhető.
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 objektumok a VIPER-hez manuálisan (tárolt capture tulajdonságokkal rendelkező osztály) vagy a Cuckoo / Mockingbird könyvtárak segítségével hozhatók létre. A manuális mock osztályok egyszerűbbek és érthetőbbek, különösen az új fejlesztők betanításához. Minden mock tárolja a rögzített értékeket (capturedUserId, displayedName) és a hívási jelzőket (didShowLoading). A teszt végén nemcsak azt ellenőrzik, hogy a metódus meghívásra került-e, hanem azt is, hogy milyen paraméterekkel — ez biztonságot ad az adatáramlás helyességéről.
Kódlefedettség az IT Sectr VIPER projektjeiben eléri a 85–95%-ot az Interactor, 90–95%-ot a Presenter, 70–80%-ot a Router, 30–50%-ot a View (UI teszteken keresztül) esetében. A View képernyőkép tesztekkel (SnapshotTesting, 1,5K csillag) tesztelhető — ez gyorsabb, mint az XCUITest, és több esetet fed le. A VIPER projekt teljes lefedettsége általában 70–80%, ami magasabb, mint egy MVVM projekté (50–65%), de több időt igényel a tesztek megírására (a fejlesztési idő 30–40%-a szemben az MVVM 20–25%-ával).
Gyakran Ismételt Kérdések
Minimum 11 fájl: 5 protokoll (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 megvalósítás (ViewController, Interactor, Presenter, Router, Entity) és Builder/Assembler. Mapper (Formatter) esetén — 12–13. Egy 50 képernyős projekt esetén ez 550–650 fájl csak a VIPER modulok számára. Az MVVM 3 fájlt igényel képernyőnként (ViewModel, View, Model) — 150 fájlt 50 képernyőhöz.
Igen, elméletileg a VIPER átvihető Androidra, de a gyakorlatban nem alkalmazzák — a Google a Jetpack MVVM-et ajánlja. A VIPER az iOS UIKithez készült, ahol a ViewController nehezen tesztelhető az életciklusa miatt. Androidon a Jetpack ViewModel megoldja a tesztelés problémáját a VIPER elkülönítés nélkül. A VIPER Android megfelelője — a Clean Architecture module/feature felosztással.
A VIPER a Clean Architecture iOS-specifikus megvalósítása. Az Interactor megfelel a Use Case-nek, az Entity — a Domain Model-nek, a Presenter — a Presentation rétegnek. A Clean Architecture hozzáad egy Gateway/Repository réteget az Interactor és az adatok közé, amely a VIPER-ben általában az Interactoron belül van megvalósítva. A Clean Architecture nem ír elő Router-t — a navigáció a megvalósítás döntésére van bízva.
Nem — a SwiftUI az MVVM + Combine-ra lett tervezve. A VIPER SwiftUI-ben túlzás: öt komponens egy képernyőhöz deklaratív UI-val többletköltség előny nélkül. SwiftUI-hez válassza az MVVM-et vagy a TCA-t (The Composable Architecture). A VIPER továbbra is releváns a UIKit örökölt kódhoz és azon projektekhez, ahol az iOS 12 és alacsonyabb a minimális verzió.
A Routern keresztül. Az A modul meghívja a router.navigateToProfile(userId: id) metódust. Az A Router létrehozza a B modult a Builderen keresztül, továbbítja a userId-t. Visszaküldés — delegálton keresztül: a B modul meghatározza a ModuleBDelegate protokollt, az A modul megvalósítja azt, és a Routern keresztül továbbítja. Rendszeresemények (kijelentkezés) — a NotificationCenteren keresztül.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is