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 се тества с тестове на екранни снимки (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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също