VIPER:关键概念,View-Interactor-Presenter-Entity-Router 模式

作者: IT Sectr 发布日期: 2026-02-16 阅读时间: 9 分钟

VIPER(View-Interactor-Presenter-Entity-Router)——由 Mutual Mobile 公司为 iOS 应用程序开发的模块化架构。VIPER 将应用程序分为五个层:View 负责显示,Interactor 负责业务逻辑,Presenter 负责数据准备,Entity 负责数据模型,Router 负责模块之间的导航。VIPER 是移动架构中单一职责原则最详细的实现。更多信息——参见 objc.io 上的文章

要点

  • VIPER——五个组件:View、Interactor、Presenter、Entity、Router,职责边界清晰
  • 模块化——每个屏幕(模块)相互隔离,通过协议进行通信
  • Router——将导航从 Presenter 中分离出来,解决了 iOS 导航问题
  • Interactor——包含业务逻辑,不依赖于 UIKit,可通过单元测试进行测试
  • iOS 原生——VIPER 是在 SwiftUI 出现之前为 UIKit 创建的,至今仍是大型 iOS 项目的标准

什么是 VIPER:模块化架构的五个组件

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 个文件。

Swift 中的 VIPER:模块、Router 和 Presenter

VIPER 模块的构建在 Builder(或 Assembler)中执行,它创建所有五个组件并通过协议连接它们。Builder 是组件彼此了解具体类型的唯一位置。构建完成后,View 被返回给外部显示,链条的其余部分是干净的并且可以隔离测试。

swift
// 协议 — 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 模块之间的通信

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 → BuildernavigateToProfile(userId:)
数据返回DelegatedidSelectCity(_:)
系统通知NotificationCenterUserDidLogout
来自 Interactor 的事件Presenter → ViewWebSocket message

NotificationCenter 用于同时影响多个模块的系统事件(注销、资费变更、推送通知)。Router 或 AppDelegate 订阅 Notification,创建所需模块或更新状态。VIPER 不禁止 NotificationCenter——重要的是它仅用于一对多事件,而一对一通信则使用委托或闭包。

VIPER 与 MVVM 和 Clean Architecture 的比较

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 模块

VIPER 专为测试而设计——每个组件都通过协议进行隔离测试。Interactor 使用模拟服务进行测试:检查 fetchUser 是否以正确的 ID 调用,以及结果是否传递给 Presenter。Presenter 使用 View 和 Interactor 的模拟对象进行测试。Router 使用模拟导航进行测试:检查 navigateToProfile 是否以正确的 userId 调用,以及是否创建了正确的模块。View 通过 UI 测试(XCUITest)进行测试。

swift
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%)。

常见问题

一个 VIPER 模块有多少个文件?

最少 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 上使用吗?

理论上,VIPER 可以移植到 Android,但在实践中并未应用——Google 推荐使用 Jetpack 的 MVVM。VIPER 是为 iOS UIKit 创建的,其中 ViewController 由于其生命周期而难以测试。在 Android 上,Jetpack ViewModel 无需 VIPER 隔离即可解决测试问题。VIPER 在 Android 上的对应物是——具有 module/feature 划分的 Clean Architecture。

VIPER 和 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 项目需要 VIPER 吗?

不需要——SwiftUI 是为 MVVM + Combine 设计的。SwiftUI 中的 VIPER 是多余的:声明式 UI 下每个屏幕五个组件是毫无益处的开销。对于 SwiftUI,请选择 MVVM 或 TCA(The Composable Architecture)。VIPER 仍然适用于 UIKit 遗留代码以及 iOS 12 及更低版本为最低版本的项目。

如何在 VIPER 模块之间传递数据?

通过 Router。模块 A 调用 router.navigateToProfile(userId: id)。Router A 通过 Builder 创建模块 B,传递 userId。反向传递——通过委托:模块 B 定义 ModuleBDelegate 协议,模块 A 实现它并通过 Router 传递。系统事件(注销)——通过 NotificationCenter。

总结

  • VIPER——五个组件严格分离:View、Interactor、Presenter、Entity、Router
  • 模块化——每个屏幕隔离,Builder 通过构造函数注入组装依赖
  • Router——将导航从 Presenter 中分离出来,解决 iOS 导航问题
  • Interactor——纯业务逻辑,不含 UIKit,通过单元测试测试
  • 代码量——每个屏幕 11+ 个文件,开发速度比 MVVM 慢 20–30%
  • 测试——70–80% 覆盖率,Interactor 和 Presenter 通过模拟对象测试
  • SwiftUI 与 UIKit——VIPER 用于 UIKit(遗留代码),MVVM/TCA 用于 SwiftUI

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读