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. Data flow: Module A → Router A → Module B Builder → Module B створюється і відкривається.
Передача даних назад (наприклад, вибрали місто на екрані вибору → повернули на екран редагування профілю) в VIPER реалізується через делегати або замикання. Модуль B визначає протокол ModuleBDelegate з методом didSelectCity(_ city: City). Модуль A реалізує цей протокол. Router A передає делегат в Module B Builder. При виборі міста Module B викликає delegate?.didSelectCity(city). Це стандартна iOS-практика, знайома будь-якому розробнику UIKit.
| Сценарій | Механізм | Приклад |
|---|---|---|
| Перехід вперед | Router → Builder | navigateToProfile(userId:) |
| Передача даних назад | Delegate | didSelectCity(_:) |
| Сповіщення системи | NotificationCenter | UserDidLogout |
| Подія з Interactor | Presenter → View | WebSocket повідомлення |
NotificationCenter використовується для системних подій (розлогінювання, зміна тарифу, пуш-сповіщення), які зачіпають кілька модулів одночасно. Router або AppDelegate підписується на Notification, створює потрібний модуль або оновлює стан. VIPER не забороняє NotificationCenter — важливо, щоб він використовувався тільки для подій 1-to-many, а для 1-to-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-only (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 створюються вручну (клас зі збереженими властивостями-захопленнями) або через бібліотеки Cuckoo / Mockingbird. Ручні mock-класи простіші та зрозуміліші, особливо для навчання нових розробників. Кожен mock зберігає captured-значення (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 — iOS-специфічна реалізація Clean Architecture. Interactor відповідає Use Case, Entity — Domain Model, Presenter — Presentation-шар. Clean Architecture додає Repository/Gateway між Interactor та даними, які в VIPER зазвичай реалізовані всередині Interactor. Clean Architecture не приписує Router — навігація залишається на розсуд реалізації.
Ні — SwiftUI спроектований під MVVM + Combine. VIPER у SwiftUI надлишковий: п'ять компонентів на один екран при декларативному UI — це оверхед без вигоди. Для 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також