VIPER: kulcsfogalmak, a View-Interactor-Presenter-Entity-Router minta

Szerző: IT Sectr Megjelenés: 2026-02-16 Olvasási idő: 9 perc

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 — öt komponens: View, Interactor, Presenter, Entity, Router egyértelmű felelősségi határokkal
  • Modularitás — minden képernyő (modul) elkülönített, kommunikáció protokollokon keresztül
  • Router — kiveszi a navigációt a Presenterből, megoldva az iOS navigációs problémáját
  • Interactor — üzleti logikát tartalmaz és nem függ a UIKit-től, unit tesztekkel tesztelhető
  • iOS-native — a VIPER a UIKithez készült a SwiftUI megjelenése előtt, és továbbra is szabvány a nagy iOS projektekhez

Mi a VIPER: a moduláris architektúra öt komponense

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.

KomponensFelelősségFüggőségek
ViewMegjelenítés, animációk, gesztusokUIKit (csak View)
InteractorÜzleti logika, hálózat, adatbázisEntity, szolgáltatások
PresenterAdatok formázása, View parancsokViewProtocol, Interactor
EntityAdatmodellekNincs
RouterNavigáció, modulok létrehozásaUIViewController (á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.

VIPER Swiftben: modul, Router és Presenter

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ő.

swift
// 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.

Kommunikáció a VIPER modulok között

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önyvMechanizmusPélda
Előre irányuló átmenetRouter → BuildernavigateToProfile(userId:)
Adatok visszaküldéseDelegatedidSelectCity(_:)
RendszerértesítésNotificationCenterUserDidLogout
Esemény az InteractorbólPresenter → ViewWebSocket 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.

A VIPER összehasonlítása az MVVM-mel és a Clean Architecture-tel

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.

VIPER modulok tesztelése

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ő.

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 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

Hány fájlból áll egy VIPER modul?

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.

Használható a VIPER Androidon?

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.

Mi a különbség a VIPER és a Clean Architecture között?

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.

Szükséges a VIPER SwiftUI projektekhez?

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ó.

Hogyan lehet adatokat továbbítani a VIPER modulok között?

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ó

  • VIPER — öt komponens szigorú elválasztással: View, Interactor, Presenter, Entity, Router
  • Modularitás — minden képernyő elkülönített, a Builder constructor injection segítségével állítja össze a függőségeket
  • Router — kiveszi a navigációt a Presenterből, megoldva a navigációs problémát iOS-en
  • Interactor — tiszta üzleti logika UIKit nélkül, unit tesztekkel tesztelhető
  • Kód mennyisége — 11+ fájl képernyőnként, fejlesztés 20–30%-kal lassabb, mint MVVM
  • Tesztelés — 70–80% lefedettség, Interactor és Presenter mock objektumokon keresztül tesztelve
  • SwiftUI vs UIKit — VIPER UIKithez (örökölt kód), MVVM/TCA SwiftUI-hoz

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.

Projekt megbeszélése

Olvassa el is