VIPER (View-Interactor-Presenter-Entity-Router) — модуларна архитектура развијена у компанији Mutual Mobile за iOS апликације. VIPER дели апликацију на пет слојева: View је задужен за приказ, Interactor — за пословну логику, Presenter — за припрему података, Entity — за моделе података, Router — за навигацију између модула. VIPER је најдетаљнија имплементација принципа јединствене одговорности међу мобилним архитектурама. Више — у чланку на objc.io.
Главно
VIPER (View-Interactor-Presenter-Entity-Router) — архитектурални образац развијен 2013–2014. године у компанији Mutual Mobile за велике iOS пројекте. Сваки екран апликације је засебан модул од пет компоненти са строго дефинисаним одговорностима. VIPER је најстрожа имплементација принципа јединствене одговорности (Single Responsibility Principle) у мобилном развоју: ниједна компонента не ради оно што може да уради друга.
View — пасивна компонента задужена само за приказ података које јој проследи Presenter. View не садржи пословну логику, не обрађује навигацију, не позива мрежне захтеве. У iOS-у — UIViewController са протоколом ViewProtocol. Interactor — слој пословне логике који ради са Entity-ем и сервисима (мрежа, база података, GPS). Interactor не импортује UIKit. Presenter — посредник између View-а и Interactor-а: прима податке од Interactor-а, форматира их за приказ, прослеђује View-у. Presenter такође не импортује UIKit. Entity — модели података (struct, class). Router — управља навигацијом: креира модуле, отвара екране, прослеђује податке између модула.
| Компонента | Одговорност | Зависности |
|---|---|---|
| View | Приказ, анимације, гестови | UIKit (само View) |
| Interactor | Пословна логика, мрежа, база података | Entity, сервиси |
| Presenter | Форматирање података, команде View-у | ViewProtocol, Interactor |
| Entity | Модели података | Нема |
| Router | Навигација, креирање модула | UIViewController (за прелазе) |
Везе између компоненти су описане протоколима. ViewProtocol дефинише методе приказа, InteractorProtocol — методе пословне логике, PresenterProtocol — методе обраде догађаја, RouterProtocol — методе навигације. Свака компонента комуницира са другом само преко протокола, што омогућава лаку замену имплементација и изоловано тестирање. У просеку VIPER модул од једног екрана садржи 5 протокола + 5 класа + 1 Builder/Assembler = 11 датотека по екрану.
Изградња VIPER модула се обавља у Builder-у (или Assembler-у), који креира свих пет компоненти и повезује их преко протокола. Builder је једино место где компоненте знају конкретне типове једна за другу. Након изградње, View се враћа напоље за приказ, остатак ланца је чист и тестира се изоловано.
// Протокол — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// Протокол — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// Протокол — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — пословна логика
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 — припрема података
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):
// обрада грешке
}
}
}
}
// Router — навигација
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 — изградња модула
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 — кључни елемент VIPER-а који имплементира ручно убризгавање зависности (Dependency Injection). Убризгавање зависности кроз конструктор (constructor injection) гарантује да компонента не може бити креирана без својих зависности. У савременом VIPER-у Builder може користити Swinject (DI контејнер), али ручна изградња остаје транспарентнија за тестирање. У IT Sectr-у примењујемо VIPER са ручном изградњом за модуле са сложеном логиком — то поједностављује читање кода новим програмерима.
Mapper (Formatter) — опциона шеста компонента VIPER-а. Mapper претвара Entity (моделе базе/сервера) у ViewModel (моделе приказа). Entity садржи UserDTO са пољима id, first_name, last_name, email. ViewModel — UserDisplayItem са name (first_name + last_name) и email. Mapper се извршава у Presenter-у. Ако је мапирање података сложено (више Entity → један ViewModel), Mapper се издваја у засебну класу ради тестирања.
VIPER модули су изоловани и не знају један за другог. Комуникација између модула се одвија преко Router-а. Када корисник кликне на дугме „Профил" на екрану корисника, Presenter позива router.navigateToProfile(userId: 42). Router креира нови модул преко ProfileModuleBuilder.build(userId: 42) и отвара га преко navigationController.push. Ток података: Модул A → Router A → Модул B Builder → Модул B се креира и отвара.
Прослеђивање података назад (нпр. изабран је град на екрану избора → враћен на екран за уређивање профила) у VIPER-у се реализује кроз делегате или затварања (closure). Модул B дефинише протокол ModuleBDelegate са методом didSelectCity(_ city: City). Модул A имплементира овај протокол. Router A прослеђује делегата у Module B Builder. При избору града, Модул B позива delegate?.didSelectCity(city). Ово је стандардна iOS пракса, позната сваком UIKit програмеру.
| Сценарио | Механизам | Пример |
|---|---|---|
| Прелаз напред | Router → Builder | navigateToProfile(userId:) |
| Прослеђивање података назад | Delegate | didSelectCity(_:) |
| Системско обавештење | NotificationCenter | UserDidLogout |
| Догађај из Interactor-а | Presenter → View | WebSocket message |
NotificationCenter се користи за системске догађаје (одјава, промена тарифе, push обавештења) који истовремено погађају више модула. Router или AppDelegate се претплаћује на Notification, креира потребни модул или ажурира стање. VIPER не забрањује NotificationCenter — важно је да се користи само за догађаје 1-према-више, а за комуникацију 1-на-1 примењују делегати или затварања.
VIPER vs MVVM — VIPER захтева 2–3 пута више кода по екрану, али пружа апсолутну изолацију компоненти. MVVM са ViewModel + SwiftUI је једноставнији и бржи, али се лошије скалира на тимове од 5+ програмера. VIPER строго одређује ко је за шта задужен: Interactor — само пословна логика, Presenter — форматирање, Router — навигација. У MVVM-у ViewModel често нарасте, преузимајући навигацију и пословну логику.
VIPER vs Clean Architecture — VIPER је посебан случај Clean Architecture-е, прилагођен за iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller у терминима Роберта Мартина. Clean Architecture додаје Gateway/Repository слој између Interactor-а и података, који се у VIPER-у обично не издваја. За савремене пројекте на SwiftUI-ју већина тимова бира Clean Architecture (The Composable Architecture) или MVVM, остављајући VIPER за заоставштину на UIKit-у.
Када изабрати VIPER — тимови од 5 програмера, пројекат на UIKit-у од 50 екрана, захтеви за тестирањем изнад 80%, само iOS (VIPER се не преноси на Android без преписивања). VIPER пружа предвидљиву структуру: нови програмер разуме модул за 15 минута. Међутим, брзина развоја је 20–30% нижа у поређењу са MVVM због већег броја датотека. У IT Sectr-у користимо VIPER за enterprise пројекте на UIKit-у са тимовима од 3 особе и дајемо предност Clean Architecture-и за нове пројекте на SwiftUI-ју.
VIPER је дизајниран за тестирање — свака компонента се тестира изоловано преко протокола. Interactor се тестира са mock-сервисима: проверава се да ли је fetchUser позван са тачним ID-јем и да ли је резултат прослеђен Presenter-у. Presenter се тестира са mock-објектима View и Interactor-а. Router се тестира са mock-навигацијом: проверава се да ли је navigateToProfile позван са тачним userId-јем и да ли је креиран исправан модул. View се тестира UI тестовима (XCUITest).
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-објекти за VIPER се креирају ручно (класа са ускладиштеним capture-својствима) или преко библиотека Cuckoo / Mockingbird. Ручне mock-класе су једноставније и разумљивије, посебно за обучавање нових програмера. Сваки mock чува снимљене вредности (capturedUserId, displayedName) и заставице позива (didShowLoading). На крају теста проверава се не само да ли је метод позван, већ и са којим параметрима — то даје сигурност у исправност тока података.
Покривеност кода у VIPER пројектима IT Sectr-а достиже 85–95% за Interactor, 90–95% за Presenter, 70–80% за Router, 30–50% за View (кроз UI тестове). View се тестира screenshot тестовима (SnapshotTesting, 1,5K звезда) — то је брже од XCUITest-а и покрива више случајева. Укупна покривеност VIPER пројекта је обично 70–80%, што је више од MVVM пројекта (50–65%), али захтева више времена за писање тестова (30–40% времена развоја наспрам 20–25% у MVVM-у).
Често постављана питања
Минимално 11 датотека: 5 протокола (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 имплементација (ViewController, Interactor, Presenter, Router, Entity) и Builder/Assembler. Са Mapper-ом (Formatter) — 12–13. За пројекат од 50 екрана то је 550–650 датотека само VIPER модула. MVVM захтева 3 датотеке по екрану (ViewModel, View, Model) — 150 датотека за 50 екрана.
Да, теоретски VIPER је преносив на Android, али се у пракси не примењује — Google препоручује MVVM са Jetpack-ом. VIPER је креиран за iOS UIKit, где је ViewController тешко тестирати због животног циклуса. На Android-у Jetpack ViewModel решава проблем тестирања без VIPER изолације. Android аналог VIPER-а — Clean Architecture са поделом на module/feature.
VIPER је имплементација Clean Architecture-е специфична за iOS. Interactor одговара Use Case-у, Entity — Domain Model-у, Presenter — Presentation слоју. Clean Architecture додаје Repository/Gateway између Interactor-а и података, који су у VIPER-у обично имплементирани унутар Interactor-а. Clean Architecture не прописује Router — навигација остаје на дискрецију имплементације.
Не — SwiftUI је дизајниран за MVVM + Combine. VIPER у SwiftUI-ју је претерано обиман: пет компоненти за један екран са декларативним UI-јем је overhead без користи. За SwiftUI бирајте MVVM или TCA (The Composable Architecture). VIPER остаје актуелан за UIKit заоставштину и пројекте где су iOS 12 и ниже минимална верзија.
Преко Router-а. Модул A позива router.navigateToProfile(userId: id). Router A креира модул B преко Builder-а, прослеђује userId. Повратно прослеђивање — преко делегата: модул B дефинише протокол ModuleBDelegate, модул A га имплементира и прослеђује преко Router-а. Системски догађаји (одјава) — преко NotificationCenter-а.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође