VIPER (View-Interactor-Presenter-Entity-Router) — modular na arkitektura na binuo sa kompanyang Mutual Mobile para sa mga iOS application. Hinahati ng VIPER ang application sa limang layer: View ang responsable sa pagpapakita, Interactor — sa lohika ng negosyo, Presenter — sa paghahanda ng datos, Entity — sa mga modelo ng datos, Router — sa nabigasyon sa pagitan ng mga modyul. Ang VIPER ay ang pinakadetalyadong implementasyon ng prinsipyo ng iisang responsibilidad sa mga mobile na arkitektura. Higit pa — sa artikulo sa objc.io.
Mga Pangunahing Punto
VIPER (View-Interactor-Presenter-Entity-Router) — isang pattern ng arkitektura na binuo noong 2013–2014 sa kompanyang Mutual Mobile para sa malalaking proyekto sa iOS. Bawat screen ng application ay isang hiwalay na modyul ng limang bahagi na may mahigpit na tinukoy na mga responsibilidad. Ang VIPER ay ang pinakamahigpit na implementasyon ng prinsipyo ng iisang responsibilidad (Single Responsibility Principle) sa mobile development: walang bahagi ang gumagawa ng kayang gawin ng iba.
View — passive na bahagi na responsable lamang sa pagpapakita ng datos na ipinadala ng Presenter. Ang View ay hindi naglalaman ng lohika ng negosyo, hindi humahawak ng nabigasyon, hindi gumagawa ng mga network request. Sa iOS — UIViewController na may protokol na ViewProtocol. Interactor — layer ng lohika ng negosyo na gumagana sa Entity at mga serbisyo (network, database, GPS). Hindi ini-import ng Interactor ang UIKit. Presenter — tagapamagitan sa pagitan ng View at Interactor: tumatanggap ng datos mula sa Interactor, nagfo-format para sa pagpapakita, ipinapadala sa View. Hindi rin ini-import ng Presenter ang UIKit. Entity — mga modelo ng datos (struct, class). Router — namamahala ng nabigasyon: gumagawa ng mga modyul, nagbubukas ng mga screen, nagpapadala ng datos sa pagitan ng mga modyul.
| Bahagi | Responsibilidad | Mga dependency |
|---|---|---|
| View | Pagpapakita, animasyon, kilos | UIKit (View lang) |
| Interactor | Lohika ng negosyo, network, database | Entity, mga serbisyo |
| Presenter | Pag-format ng datos, mga utos ng View | ViewProtocol, Interactor |
| Entity | Mga modelo ng datos | Wala |
| Router | Nabigasyon, paggawa ng modyul | UIViewController (para sa transisyon) |
Mga koneksyon sa pagitan ng mga bahagi ay inilalarawan ng mga protokol. Tinutukoy ng ViewProtocol ang mga pamamaraan ng pagpapakita, InteractorProtocol — mga pamamaraan ng lohika ng negosyo, PresenterProtocol — mga pamamaraan ng paghawak ng kaganapan, RouterProtocol — mga pamamaraan ng nabigasyon. Bawat bahagi ay nakikipag-ugnayan sa isa pa sa pamamagitan lamang ng protokol, na nagpapahintulot ng madaling pagpapalit ng mga implementasyon at nakahiwalay na pagsubok. Ang karaniwang modyul ng VIPER mula sa isang screen ay naglalaman ng 5 protokol + 5 klase + 1 Builder/Assembler = 11 file bawat screen.
Pagbuo ng modyul ng VIPER ay isinasagawa sa Builder (o Assembler), na lumilikha ng lahat ng limang bahagi at iniuugnay ang mga ito sa pamamagitan ng mga protokol. Ang Builder ay ang tanging lugar kung saan alam ng mga bahagi ang mga kongkretong uri ng bawat isa. Pagkatapos ng pagbuo, ang View ay ibabalik sa labas para sa pagpapakita, ang natitirang chain ay malinis at sinusuri nang nakahiwalay.
// Protokol — View
protocol UserViewProtocol: AnyObject {
func display(name: String)
func display(email: String)
func showLoading()
func hideLoading()
}
// Protokol — Interactor
protocol UserInteractorProtocol {
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void)
}
// Protokol — Router
protocol UserRouterProtocol {
func navigateToProfile(userId: Int)
}
// Interactor — lohika ng negosyo
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 — paghahanda ng datos
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):
// paghawak ng error
}
}
}
}
// Router — nabigasyon
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 — pagbuo ng modyul
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 — ang pangunahing elemento ng VIPER na nagpapatupad ng manu-manong Dependency Injection. Ang pag-iniksyon ng dependency sa pamamagitan ng constructor (constructor injection) ay ginagarantiyang hindi malilikha ang isang bahagi nang wala ang mga dependency nito. Sa modernong VIPER, ang Builder ay maaaring gumamit ng Swinject (DI container), ngunit ang manu-manong pagbuo ay nananatiling mas transparent para sa pagsubok. Sa IT Sectr inilalapat namin ang VIPER na may manu-manong pagbuo para sa mga modyul na may kumplikadong lohika — pinapasimple nito ang pagbabasa ng code para sa mga bagong developer.
Mapper (Formatter) — isang opsyonal na ikaanim na bahagi ng VIPER. Ang Mapper ay nagko-convert ng Entity (mga modelo ng database/server) sa ViewModel (mga modelo ng pagpapakita). Ang Entity ay naglalaman ng UserDTO na may mga field na id, first_name, last_name, email. ViewModel — UserDisplayItem na may name (first_name + last_name) at email. Ang Mapper ay isinasagawa sa Presenter. Kung kumplikado ang pagmamapa ng datos (maramihang Entity → isang ViewModel), ang Mapper ay inililipat sa isang hiwalay na klase para sa pagsubok.
Ang mga modyul ng VIPER ay nakahiwalay at hindi alam ang tungkol sa isa't isa. Ang komunikasyon sa pagitan ng mga modyul ay nangyayari sa pamamagitan ng Router. Kapag pinindot ng gumagamit ang button na „Profil" sa screen ng gumagamit, tinatawag ng Presenter ang router.navigateToProfile(userId: 42). Gumagawa ang Router ng bagong modyul sa pamamagitan ng ProfileModuleBuilder.build(userId: 42) at bubuksan ito sa pamamagitan ng navigationController.push. Daloy ng datos: Modyul A → Router A → Modyul B Builder → Modyul B ay ginawa at binuksan.
Pagpapadala ng datos pabalik (halimbawa, napili ang isang lungsod sa screen ng pagpili → ibinalik sa screen ng pag-edit ng profile) sa VIPER ay ipinapatupad sa pamamagitan ng mga delegado o closure. Tinutukoy ng Modyul B ang protokol na ModuleBDelegate na may pamamaraang didSelectCity(_ city: City). Ipinapatupad ng Modyul A ang protokol na ito. Ipinapadala ng Router A ang delegado sa Module B Builder. Sa pagpili ng lungsod, tinatawag ng Modyul B ang delegate?.didSelectCity(city). Ito ay isang pamantayang kasanayan sa iOS, pamilyar sa bawat developer ng UIKit.
| Scenario | Mekanismo | Halimbawa |
|---|---|---|
| Transisyon pasulong | Router → Builder | navigateToProfile(userId:) |
| Pagpapadala ng datos pabalik | Delegate | didSelectCity(_:) |
| Notipikasyon ng sistema | NotificationCenter | UserDidLogout |
| Kaganapan mula sa Interactor | Presenter → View | WebSocket message |
NotificationCenter ay ginagamit para sa mga kaganapan ng sistema (pag-logout, pagbabago ng taripa, push notipikasyon) na sabay na nakakaapekto sa maraming modyul. Ang Router o AppDelegate ay nag-subscribe sa Notification, gumagawa ng kinakailangang modyul o nag-a-update ng estado. Hindi pinagbabawalan ng VIPER ang NotificationCenter — mahalaga na ito ay gamitin lamang para sa mga kaganapang 1-sa-marami, at para sa komunikasyong 1-sa-1 ay gumamit ng mga delegado o closure.
VIPER vs MVVM — Ang VIPER ay nangangailangan ng 2–3 beses na mas maraming code bawat screen, ngunit nagbibigay ng ganap na paghihiwalay ng mga bahagi. Ang MVVM na may ViewModel + SwiftUI ay mas simple at mas mabilis, ngunit mas mahina ang pag-scale sa mga team na 5+ developer. Mahigpit na tinutukoy ng VIPER kung sino ang responsable para sa ano: Interactor — lohika ng negosyo lamang, Presenter — pag-format, Router — nabigasyon. Sa MVVM, ang ViewModel ay madalas lumalaki, na inaako ang nabigasyon at lohika ng negosyo.
VIPER vs Clean Architecture — Ang VIPER ay isang partikular na kaso ng Clean Architecture, na inangkop para sa iOS UIKit. Interactor = Use Case, Entity = Domain Model, Presenter = Presentation, Router = Controller sa terminolohiya ni Robert Martin. Ang Clean Architecture ay nagdaragdag ng layer ng Gateway/Repository sa pagitan ng Interactor at datos, na sa VIPER ay karaniwang hindi pinaghihiwalay. Para sa mga modernong proyekto sa SwiftUI, karamihan ng mga team ay pumipili ng Clean Architecture (The Composable Architecture) o MVVM, iniiwan ang VIPER para sa legacy code sa UIKit.
Kailan pumili ng VIPER — mga team mula 5 developer, proyekto sa UIKit mula 50 screen, mga kinakailangan sa pagsubok na higit sa 80%, iOS lamang (hindi naililipat ang VIPER sa Android nang walang pagsusulat muli). Nagbibigay ang VIPER ng predictable na istraktura: nauunawaan ng bagong developer ang modyul sa loob ng 15 minuto. Gayunpaman, ang bilis ng pag-develop ay 20–30% na mas mababa kumpara sa MVVM dahil sa mas maraming bilang ng mga file. Sa IT Sectr ginagamit namin ang VIPER para sa enterprise na mga proyekto sa UIKit na may mga team mula 3 tao at mas pinipili ang Clean Architecture para sa mga bagong proyekto sa SwiftUI.
Ang VIPER ay dinisenyo para sa pagsubok — bawat bahagi ay sinusuri nang nakahiwalay sa pamamagitan ng mga protokol. Ang Interactor ay sinusuri gamit ang mga mock service: sinusuri kung ang fetchUser ay tinawag na may tamang ID at kung ang resulta ay ipinadala sa Presenter. Ang Presenter ay sinusuri gamit ang mga mock na bagay ng View at Interactor. Ang Router ay sinusuri gamit ang mock nabigasyon: sinusuri kung ang navigateToProfile ay tinawag na may tamang userId at kung ang tamang modyul ay ginawa. Ang View ay sinusuri gamit ang mga UI test (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)
}
}
Mga mock na bagay para sa VIPER ay ginagawa nang manu-mano (klase na may mga nakaimbak na capture property) o sa pamamagitan ng mga library na Cuckoo / Mockingbird. Ang mga manual na mock na klase ay mas simple at mas madaling maunawaan, lalo na para sa pagsasanay ng mga bagong developer. Bawat mock ay nag-iimbak ng mga nakuha na halaga (capturedUserId, displayedName) at mga flag ng tawag (didShowLoading). Sa dulo ng pagsubok, sinusuri hindi lamang kung ang pamamaraan ay tinawag, kundi pati na rin kung anong mga parameter ang ginamit — ito ay nagbibigay ng kumpyansa sa kawastuhan ng daloy ng datos.
Saklaw ng code sa mga proyektong VIPER ng IT Sectr ay umaabot sa 85–95% para sa Interactor, 90–95% para sa Presenter, 70–80% para sa Router, 30–50% para sa View (sa pamamagitan ng UI test). Ang View ay sinusuri gamit ang mga screenshot test (SnapshotTesting, 1.5K bituin) — ito ay mas mabilis kaysa sa XCUITest at sumasaklaw ng mas maraming kaso. Ang kabuuang saklaw ng proyektong VIPER ay karaniwang 70–80%, na mas mataas kaysa sa proyektong MVVM (50–65%), ngunit nangangailangan ng mas maraming oras sa pagsulat ng mga test (30–40% ng oras ng pag-develop kumpara sa 20–25% sa MVVM).
Mga Madalas Itanong
Minimum na 11 file: 5 protokol (ViewProtocol, InteractorProtocol, PresenterProtocol, RouterProtocol, Entity), 5 implementasyon (ViewController, Interactor, Presenter, Router, Entity) at Builder/Assembler. May Mapper (Formatter) — 12–13. Para sa proyektong may 50 screen, ito ay 550–650 file para lamang sa mga modyul ng VIPER. Ang MVVM ay nangangailangan ng 3 file bawat screen (ViewModel, View, Model) — 150 file para sa 50 screen.
Oo, sa teorya ay naililipat ang VIPER sa Android, ngunit sa praktika ay hindi ginagamit — inirerekomenda ng Google ang MVVM na may Jetpack. Ginawa ang VIPER para sa iOS UIKit, kung saan ang ViewController ay mahirap subukan dahil sa siklo ng buhay. Sa Android, nilulutas ng Jetpack ViewModel ang problema sa pagsubok nang walang paghihiwalay ng VIPER. Ang katumbas ng VIPER sa Android — Clean Architecture na may paghahati sa module/feature.
Ang VIPER ay isang implementasyon ng Clean Architecture na tiyak sa iOS. Ang Interactor ay tumutugma sa Use Case, Entity — Domain Model, Presenter — layer ng Presentation. Ang Clean Architecture ay nagdaragdag ng Gateway/Repository sa pagitan ng Interactor at datos, na sa VIPER ay karaniwang ipinapatupad sa loob ng Interactor. Hindi nagtatakda ang Clean Architecture ng Router — ang nabigasyon ay naiwan sa pagpapasya ng implementasyon.
Hindi — ang SwiftUI ay dinisenyo para sa MVVM + Combine. Ang VIPER sa SwiftUI ay labis: limang bahagi para sa isang screen na may deklaratibong UI ay overhead na walang pakinabang. Para sa SwiftUI pumili ng MVVM o TCA (The Composable Architecture). Ang VIPER ay nananatiling may kaugnayan para sa legacy code ng UIKit at mga proyekto kung saan ang iOS 12 at mas mababa ay ang pinakamababang bersyon.
Sa pamamagitan ng Router. Ang Modyul A ay tumatawag ng router.navigateToProfile(userId: id). Gumagawa ang Router A ng modyul B sa pamamagitan ng Builder, ipinapadala ang userId. Pagpapadala pabalik — sa pamamagitan ng delegado: tinutukoy ng Modyul B ang protokol na ModuleBDelegate, ipinapatupad ito ng Modyul A at ipinapadala sa pamamagitan ng Router. Mga kaganapan ng sistema (pag-logout) — sa pamamagitan ng NotificationCenter.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din