VIPER: concepte cheie, pattern-ul View-Interactor-Presenter-Entity-Router

Autor: IT Sectr Publicat: 2026-02-16 Timp de citire: 9 min

VIPER (View-Interactor-Presenter-Entity-Router) — arhitectura modulară dezvoltată în compania Mutual Mobile pentru aplicațiile iOS. VIPER împarte aplicația în cinci straturi: View răspunde de afișare, Interactor — de logica de afaceri, Presenter — de pregătirea datelor, Entity — de modelele de date, Router — de navigarea între module. VIPER este cea mai detaliată implementare a principiului responsabilității unice printre arhitecturile mobile. Mai multe — în articolul de pe objc.io.

Principalele

  • VIPER — cinci componente: View, Interactor, Presenter, Entity, Router cu limite clare de responsabilitate
  • Modularitate — fiecare ecran (modul) este izolat, comunicarea prin protocoale
  • Router — externalizează navigarea din Presenter, rezolvând problema navigării iOS
  • Interactor — conține logica de afaceri și nu depinde de UIKit, se testează cu teste unitare
  • iOS-native — VIPER a fost creat pentru UIKit înainte de apariția SwiftUI și rămâne standardul pentru proiectele iOS mari

Ce este VIPER: cinci componente ale arhitecturii modulare

VIPER (View-Interactor-Presenter-Entity-Router) — un pattern arhitectural dezvoltat în 2013–2014 în compania Mutual Mobile pentru proiecte iOS mari. Fiecare ecran al aplicației este un modul separat din cinci componente cu responsabilități strict definite. VIPER este cea mai strictă implementare a principiului responsabilității unice (Single Responsibility Principle) în dezvoltarea mobilă: niciun component nu face ceea ce poate face altul.

View — component pasiv responsabil doar de afișarea datelor transmise de Presenter. View nu conține logică de afaceri, nu gestionează navigarea, nu face cereri de rețea. În iOS — UIViewController cu protocolul ViewProtocol. Interactor — stratul de logică de afaceri, care lucrează cu Entity și servicii (rețea, baze de date, GPS). Interactor nu importă UIKit. Presenter — intermediar între View și Interactor: primește date de la Interactor, le formatează pentru afișare, le transmite View. Presenter, de asemenea, nu importă UIKit. Entity — modele de date (struct, class). Router — gestionează navigarea: creează module, deschide ecrane, transmite date între module.

ComponentResponsabilitateDependențe
ViewAfișare, animații, gesturiUIKit (doar View)
InteractorLogică de afaceri, rețea, bază de dateEntity, servicii
PresenterFormatarea datelor, comenzi ViewViewProtocol, Interactor
EntityModele de dateNu
RouterNavigare, crearea modulelorUIViewController (pentru tranziții)

Conexiunile între componente sunt descrise prin protocoale. ViewProtocol definește metodele de afișare, InteractorProtocol — metodele de logică de afaceri, PresenterProtocol — metodele de procesare a evenimentelor, RouterProtocol — metodele de navigare. Fiecare component comunică cu altul doar prin protocol, ceea ce permite înlocuirea ușoară a implementărilor și testarea izolată. În medie, un modul VIPER dintr-un ecran conține 5 protocoale + 5 clase + 1 Builder/Assembler = 11 fișiere per ecran.

VIPER în Swift: modul, Router și Presenter

Construirea modulului VIPER se realizează în Builder (sau Assembler), care creează toate cele cinci componente și le leagă prin protocoale. Builder este singurul loc unde componentele cunosc tipurile concrete unul altuia. După construire, View este returnat în exterior pentru afișare, restul lanțului este curat și se testează izolat.

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 — logică de afaceri
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 — pregătirea datelor
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):
                // gestionarea erorilor
            }
        }
    }
}

// Router — navigare
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 — construirea modulului
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 — elementul cheie al VIPER, care implementează injectarea dependențelor manual (Dependency Injection). Injectarea dependențelor prin constructor (constructor injection) garantează că un component nu poate fi creat fără dependențele sale. În VIPER modern, Builder poate folosi Swinject (container DI), dar construirea manuală rămâne mai transparentă pentru testare. În IT Sectr aplicăm VIPER cu construire manuală pentru module cu logică complexă — aceasta simplifică citirea codului de către noii dezvoltatori.

Mapper (Formatter) — o a șasea componentă opțională a VIPER. Mapper transformă Entity (modele de bază de date/server) în ViewModel (modele de afișare). Entity conține UserDTO cu câmpurile id, first_name, last_name, email. ViewModel — UserDisplayItem cu name (first_name + last_name) și email. Mapper se execută în Presenter. Dacă maparea datelor este complexă (mai multe Entity → un ViewModel), Mapper este extras într-o clasă separată pentru testare.

Comunicarea între modulele VIPER

Modulele VIPER sunt izolate și nu știu unele de altele. Comunicarea între module are loc prin Router. Când utilizatorul apasă butonul „Profil" pe ecranul utilizatorului, Presenter apelează router.navigateToProfile(userId: 42). Router creează un modul nou prin ProfileModuleBuilder.build(userId: 42) și îl deschide prin navigationController.push. Fluxul de date: Modulul A → Router A → Modulul B Builder → Modulul B este creat și deschis.

Transmiterea datelor înapoi (de exemplu, s-a selectat un oraș pe ecranul de selecție → s-a returnat pe ecranul de editare a profilului) în VIPER se implementează prin delegați sau closure-uri. Modulul B definește protocolul ModuleBDelegate cu metoda didSelectCity(_ city: City). Modulul A implementează acest protocol. Router A transmite delegatul către Module B Builder. La selectarea orașului, Modulul B apelează delegate?.didSelectCity(city). Aceasta este o practică standard iOS, cunoscută oricărui dezvoltator UIKit.

ScenariuMecanismExemplu
Tranziție înainteRouter → BuildernavigateToProfile(userId:)
Transmitere date înapoiDelegatedidSelectCity(_:)
Notificare de sistemNotificationCenterUserDidLogout
Eveniment din InteractorPresenter → ViewWebSocket message

NotificationCenter este utilizat pentru evenimente de sistem (deconectare, schimbare tarif, notificări push) care afectează mai multe module simultan. Router sau AppDelegate se abonează la Notification, creează modulul necesar sau actualizează starea. VIPER nu interzice NotificationCenter — important este să fie folosit doar pentru evenimente 1-to-many, iar pentru comunicarea 1-to-1 să se aplice delegați sau closure-uri.

Comparația VIPER cu MVVM și Clean Architecture

VIPER vs MVVM — VIPER necesită de 2–3 ori mai mult cod per ecran, dar oferă izolare absolută a componentelor. MVVM cu ViewModel + SwiftUI este mai simplu și mai rapid, dar se scalează mai greu în echipe de 5+ dezvoltatori. VIPER stabilește strict cine răspunde pentru ce: Interactor — doar logica de afaceri, Presenter — formatarea, Router — navigarea. În MVVM, ViewModel adesea crește, preluând navigarea și logica de afaceri.

VIPER vs Clean Architecture — VIPER este un caz particular al Clean Architecture, adaptat pentru iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller în termenii lui Robert Martin. Clean Architecture adaugă stratul Gateway/Repository între Interactor și date, care în VIPER de obicei nu este separat. Pentru proiecte moderne în SwiftUI, majoritatea echipelor aleg Clean Architecture (The Composable Architecture) sau MVVM, lăsând VIPER pentru codul moștenit pe UIKit.

Când să alegeți VIPER — echipe de la 5 dezvoltatori, proiect pe UIKit de la 50 de ecrane, cerințe de testare de peste 80%, doar iOS (VIPER nu se transferă pe Android fără rescriere). VIPER oferă o structură predictibilă: un dezvoltator nou înțelege modulul în 15 minute. Cu toate acestea, viteza de dezvoltare este cu 20–30% mai mică comparativ cu MVVM din cauza numărului mai mare de fișiere. În IT Sectr folosim VIPER pentru proiecte enterprise pe UIKit cu echipe de la 3 persoane și preferăm Clean Architecture pentru proiecte noi în SwiftUI.

Testarea modulelor VIPER

VIPER este proiectat pentru testare — fiecare component este testat izolat prin protocoale. Interactor se testează cu servicii mock: se verifică dacă fetchUser a fost apelat cu ID-ul corect și dacă rezultatul a fost transmis către Presenter. Presenter se testează cu obiecte mock View și Interactor. Router se testează cu navigare mock: se verifică dacă navigateToProfile a fost apelat cu userId corect și dacă a fost creat modulul corect. View se testează cu teste 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)
    }
}

Obiecte mock pentru VIPER sunt create manual (clasă cu proprietăți de captură stocate) sau prin bibliotecile Cuckoo / Mockingbird. Clasele mock manuale sunt mai simple și mai ușor de înțeles, în special pentru instruirea noilor dezvoltatori. Fiecare mock stochează valori capturate (capturedUserId, displayedName) și flag-uri de apel (didShowLoading). La sfârșitul testului se verifică nu doar că metoda a fost apelată, ci și cu ce parametri — aceasta oferă încredere în corectitudinea fluxului de date.

Acoperirea codului în proiectele VIPER ale IT Sectr atinge 85–95% pentru Interactor, 90–95% pentru Presenter, 70–80% pentru Router, 30–50% pentru View (prin teste UI). View se testează cu teste de captură de ecran (SnapshotTesting, 1,5K stele) — este mai rapid decât XCUITest și acoperă mai multe cazuri. Acoperirea totală a unui proiect VIPER este de obicei 70–80%, ceea ce este mai mare decât un proiect MVVM (50–65%), dar necesită mai mult timp pentru scrierea testelor (30–40% din timpul de dezvoltare față de 20–25% în MVVM).

Întrebări frecvente

Câte fișiere are un modul VIPER?

Minimum 11 fișiere: 5 protocoale (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementări (ViewController, Interactor, Presenter, Router, Entity) și Builder/Assembler. Cu Mapper (Formatter) — 12–13. Pentru un proiect cu 50 de ecrane, aceasta înseamnă 550–650 de fișiere doar pentru modulele VIPER. MVVM necesită 3 fișiere per ecran (ViewModel, View, Model) — 150 de fișiere pentru 50 de ecrane.

Se poate folosi VIPER pe Android?

Da, teoretic VIPER este portabil pe Android, dar în practică nu se aplică — Google recomandă MVVM cu Jetpack. VIPER a fost creat pentru iOS UIKit, unde ViewController este dificil de testat din cauza ciclului de viață. Pe Android, Jetpack ViewModel rezolvă problema testării fără izolarea VIPER. Analogul Android al VIPER — Clean Architecture cu împărțire pe module/features.

Care este diferența dintre VIPER și Clean Architecture?

VIPER este o implementare specifică iOS a Clean Architecture. Interactor corespunde Use Case, Entity — Domain Model, Presenter — stratului Presentation. Clean Architecture adaugă Repository/Gateway între Interactor și date, care în VIPER sunt de obicei implementate în interiorul Interactor. Clean Architecture nu prescrie Router — navigarea rămâne la latitudinea implementării.

Este necesar VIPER pentru proiectele SwiftUI?

Nu — SwiftUI este proiectat pentru MVVM + Combine. VIPER în SwiftUI este excesiv: cinci componente pentru un ecran cu UI declarativ este un overhead fără beneficiu. Pentru SwiftUI alegeți MVVM sau TCA (The Composable Architecture). VIPER rămâne relevant pentru codul moștenit UIKit și proiecte unde iOS 12 și mai jos este versiunea minimă.

Cum se transmit datele între modulele VIPER?

Prin Router. Modulul A apelează router.navigateToProfile(userId: id). Router A creează modulul B prin Builder, transmite userId. Transmiterea inversă — prin delegat: modulul B definește protocolul ModuleBDelegate, modulul A îl implementează și îl transmite prin Router. Evenimente de sistem (deconectare) — prin NotificationCenter.

Concluzii

  • VIPER — cinci componente cu separare strictă: View, Interactor, Presenter, Entity, Router
  • Modularitate — fiecare ecran este izolat, Builder asamblează dependențele prin constructor injection
  • Router — externalizează navigarea din Presenter, rezolvând problema navigării pe iOS
  • Interactor — logică de afaceri pură fără UIKit, testată cu teste unitare
  • Cantitatea de cod — 11+ fișiere per ecran, dezvoltare cu 20–30% mai lentă decât MVVM
  • Testare — acoperire 70–80%, Interactor și Presenter testate prin obiecte mock
  • SwiftUI vs UIKit — VIPER pentru UIKit (cod moștenit), MVVM/TCA pentru SwiftUI

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și