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 包含具有 id、first_name、last_name、email 字段的 UserDTO。ViewModel——具有 name(first_name + last_name)和 email 的 UserDisplayItem。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 中通过委托或闭包实现。模块 B 定义具有 didSelectCity(_ city: City) 方法的 ModuleBDelegate 协议。模块 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 用于同时影响多个模块的系统事件(注销、资费变更、推送通知)。Router 或 AppDelegate 订阅 Notification,创建所需模块或更新状态。VIPER 不禁止 NotificationCenter——重要的是它仅用于一对多事件,而一对一通信则使用委托或闭包。
VIPER 与 MVVM——VIPER 每个屏幕需要 2–3 倍的代码,但提供了组件的绝对隔离。带有 ViewModel + SwiftUI 的 MVVM 更简单、更快速,但在 5+ 开发人员的团队中扩展性较差。VIPER 严格确定谁负责什么:Interactor——仅业务逻辑,Presenter——格式化,Router——导航。在 MVVM 中,ViewModel 经常膨胀,接管导航和业务逻辑。
VIPER 与 Clean Architecture——VIPER 是 Clean Architecture 的一个特例,针对 iOS UIKit 进行了适配。Interactor = Use Case,Entity = Domain Model,Presenter = Presentation,Router = Controller(按 Robert Martin 的术语)。Clean Architecture 在 Interactor 和数据之间添加了 Gateway/Repository 层,这在 VIPER 中通常不分离。对于 SwiftUI 中的现代项目,大多数团队选择 Clean Architecture(The Composable Architecture)或 MVVM,将 VIPER 留给 UIKit 上的遗留代码。
何时选择 VIPER——5 名以上开发人员的团队,50 个屏幕以上的 UIKit 项目,测试要求超过 80%,仅限 iOS(VIPER 在不重写的情况下无法移植到 Android)。VIPER 提供可预测的结构:新开发人员在 15 分钟内理解一个模块。然而,由于文件数量较多,开发速度比 MVVM 慢 20–30%。在 IT Sectr,我们将 VIPER 用于 UIKit 上的企业项目(3 人以上团队),并倾向于为 SwiftUI 上的新项目选择 Clean Architecture。
VIPER 专为测试而设计——每个组件都通过协议进行隔离测试。Interactor 使用模拟服务进行测试:检查 fetchUser 是否以正确的 ID 调用,以及结果是否传递给 Presenter。Presenter 使用 View 和 Interactor 的模拟对象进行测试。Router 使用模拟导航进行测试:检查 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)可以手动创建(具有存储的捕获属性的类)或通过 Cuckoo / Mockingbird 库创建。手动模拟类更简单、更易理解,特别是对于培训新开发人员。每个模拟存储捕获的值(capturedUserId、displayedName)和调用标志(didShowLoading)。在测试结束时,不仅要检查方法是否被调用,还要检查使用了哪些参数——这确保了数据流的正确性。
代码覆盖率在 IT Sectr 的 VIPER 项目中,Interactor 达到 85–95%,Presenter 达到 90–95%,Router 达到 70–80%,View 达到 30–50%(通过 UI 测试)。View 通过屏幕截图测试(SnapshotTesting,1.5K 星标)进行测试——这比 XCUITest 更快,覆盖更多情况。VIPER 项目的总覆盖率通常在 70–80% 之间,高于 MVVM 项目(50–65%),但需要更多时间来编写测试(占开发时间的 30–40%,而 MVVM 为 20–25%)。
常见问题
最少 11 个文件:5 个协议(ViewProtocol、InteractorProtocol、PresenterProtocol、RouterProtocol、Entity),5 个实现(ViewController、Interactor、Presenter、Router、Entity)和 Builder/Assembler。加上 Mapper(Formatter)——12–13 个。对于一个 50 个屏幕的项目,仅 VIPER 模块就需要 550–650 个文件。MVVM 每个屏幕需要 3 个文件(ViewModel、View、Model)——50 个屏幕需要 150 个文件。
理论上,VIPER 可以移植到 Android,但在实践中并未应用——Google 推荐使用 Jetpack 的 MVVM。VIPER 是为 iOS UIKit 创建的,其中 ViewController 由于其生命周期而难以测试。在 Android 上,Jetpack ViewModel 无需 VIPER 隔离即可解决测试问题。VIPER 在 Android 上的对应物是——具有 module/feature 划分的 Clean Architecture。
VIPER 是 Clean Architecture 的 iOS 特定实现。Interactor 对应 Use Case,Entity 对应 Domain Model,Presenter 对应 Presentation 层。Clean Architecture 在 Interactor 和数据之间添加了 Repository/Gateway 层,这在 VIPER 中通常是在 Interactor 内部实现的。Clean Architecture 不规定 Router——导航由实现决定。
不需要——SwiftUI 是为 MVVM + Combine 设计的。SwiftUI 中的 VIPER 是多余的:声明式 UI 下每个屏幕五个组件是毫无益处的开销。对于 SwiftUI,请选择 MVVM 或 TCA(The Composable Architecture)。VIPER 仍然适用于 UIKit 遗留代码以及 iOS 12 及更低版本为最低版本的项目。
通过 Router。模块 A 调用 router.navigateToProfile(userId: id)。Router A 通过 Builder 创建模块 B,传递 userId。反向传递——通过委托:模块 B 定义 ModuleBDelegate 协议,模块 A 实现它并通过 Router 传递。系统事件(注销)——通过 NotificationCenter。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。