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 возвращается наружу для отображения, остальная цепочка чистая и тестируется изолированно.
// 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 — бизнес-логика
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 message |
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 создаются вручную (класс с хранимыми свойствами-capture) или через библиотеки 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также