一、VIPER组件关系图与核心解读
以下是VIPER组件关系图:

图表解读
- View(视图):最靠近用户的粉色框。它接收用户输入(如点击按钮),并负责展示界面。View 只与 Presenter 有直接交互:将事件传递给 Presenter(用户事件箭头),并接收 Presenter 的指令更新 UI(更新UI箭头)。
- Presenter(展示器):蓝色的中心协调者。它接收来自 View 的事件,然后决定调用 Interactor 处理业务(执行业务箭头),或者调用 Router 进行导航(请求导航箭头)。同时,它接收 Interactor 的回调结果(结果回调虚线箭头),并转换为 View 可展示的数据。
- Interactor(交互器):绿色的业务逻辑层。它实现具体的业务用例(如验证、计算、数据获取),通过 Entity 操作数据(I --> E箭头)。Interactor 不直接与 View 或 Router 通信,只通过协议向 Presenter 回传结果。
- Router(路由器):粉紫色的导航层。它负责创建模块、管理页面跳转。Presenter 请求导航时,Router 会操作 View 进行实际的跳转(执行跳转箭头指向 View)。
- Entity(实体):黄色的数据模型,仅被 Interactor 使用,是纯粹的数据结构。
依赖方向
- View → Presenter → Interactor:这是主要的控制流方向,遵循单向依赖。
- Presenter → Router:导航请求也由 Presenter 发起。
- 反向通信全部通过协议(接口)实现:例如 Interactor 通过协议回调 Presenter,Presenter 通过协议更新 View,确保各层之间只依赖抽象,不依赖具体实现。
关键点
- 组件间不直接持有具体类,而是持有遵循协议的引用,便于测试和替换。
- Interactor 是业务逻辑的核心,它不依赖 UIKit,可独立测试。
- Router 集中管理导航,避免了在 Presenter 或 View 中混杂跳转代码。
- 所有箭头都是单向的,避免了循环依赖。
二、一个简单但完整的VIPER示例:欢迎模块
我们将构建一个欢迎模块:用户输入名字,点击按钮,界面下方显示“你好,[名字]!”。这个示例将展示 VIPER 五个组件的协作方式,并附带详细注释。
项目结构概览
GreetingModule/
├── Entity
│ └── GreetingEntity.swift # 数据模型
├── Interactor
│ ├── GreetingInteractorProtocol.swift # 交互器接口
│ └── GreetingInteractor.swift # 交互器实现
├── Presenter
│ ├── GreetingPresenterProtocol.swift # 展示器接口
│ └── GreetingPresenter.swift # 展示器实现
├── View
│ ├── GreetingViewProtocol.swift # 视图接口
│ └── GreetingViewController.swift # 视图实现(UIViewController)
├── Router
│ ├── GreetingRouterProtocol.swift # 路由器接口
│ └── GreetingRouter.swift # 路由器实现
└── Builder
└── GreetingModuleBuilder.swift # 模块构建器(可选,职责分离更细)
1. Entity(实体)
Entity 是最纯粹的数据模型,只包含属性,没有任何业务逻辑。
// GreetingEntity.swift
import Foundation
/// 实体:存储用户输入的名字和生成的欢迎语
struct GreetingEntity {
let name: String // 用户输入的名字
var greetingMessage: String? // 生成的欢迎语(可选,初始为空)
}
2. Interactor(交互器)
Interactor 负责业务逻辑,它通过协议与 Presenter 通信,不依赖具体视图。
// GreetingInteractorProtocol.swift
import Foundation
/// 交互器输出协议:交互器通过此协议向Presenter发送结果
protocol GreetingInteractorOutputProtocol: AnyObject {
/// 成功生成欢迎语
func didGenerateGreeting(_ message: String)
/// 生成失败(如名字为空)
func didFailToGenerateGreeting(with error: String)
}
/// 交互器输入协议:Presenter通过此协议调用交互器
protocol GreetingInteractorInputProtocol: AnyObject {
/// 设置输出代理(通常由Presenter实现)
var presenter: GreetingInteractorOutputProtocol? { get set }
/// 执行业务:生成欢迎语
func generateGreeting(for name: String)
}
// GreetingInteractor.swift
import Foundation
/// 交互器实现
class GreetingInteractor: GreetingInteractorInputProtocol {
weak var presenter: GreetingInteractorOutputProtocol?
func generateGreeting(for name: String) {
// 业务逻辑:检查名字是否为空
guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
presenter?.didFailToGenerateGreeting(with: "名字不能为空")
return
}
// 生成欢迎语(这里可以是复杂的业务逻辑)
let message = "你好,\(name)!"
presenter?.didGenerateGreeting(message)
}
}
3. Presenter(展示器)
Presenter 接收 View 的用户事件,调用 Interactor 执行任务,并将结果格式化后通过协议更新 View。它还负责决定是否需要跳转(通过 Router)。
// GreetingPresenterProtocol.swift
import Foundation
/// 视图输出协议:View通过此协议将用户事件传递给Presenter
protocol GreetingViewOutputProtocol: AnyObject {
/// 用户点击了生成按钮
func didTapGenerateButton(with name: String?)
}
/// 视图输入协议:Presenter通过此协议更新View的UI
protocol GreetingViewInputProtocol: AnyObject {
/// 显示生成的欢迎语
func showGreeting(_ message: String)
/// 显示错误信息
func showError(_ error: String)
}
// GreetingPresenter.swift
import Foundation
/// Presenter实现
class GreetingPresenter: GreetingViewOutputProtocol {
// 通过协议持有View的弱引用,避免循环引用
weak var view: GreetingViewInputProtocol?
// 通过协议持有Interactor的引用(输入协议)
var interactor: GreetingInteractorInputProtocol?
// 通过协议持有Router的引用
var router: GreetingRouterProtocol?
// MARK: - GreetingViewOutputProtocol
func didTapGenerateButton(with name: String?) {
// 从View接收事件,调用Interactor处理业务
interactor?.generateGreeting(for: name ?? "")
}
}
/// 实现Interactor的输出协议,接收Interactor的结果
extension GreetingPresenter: GreetingInteractorOutputProtocol {
func didGenerateGreeting(_ message: String) {
// 将结果格式化为View需要的格式(这里直接使用)
view?.showGreeting(message)
}
func didFailToGenerateGreeting(with error: String) {
// 显示错误信息
view?.showError(error)
}
}
4. View(视图)
View 负责 UI 展示和用户交互,它只将事件传递给 Presenter,并通过协议接收 Presenter 的更新。
// GreetingViewController.swift
import UIKit
/// 视图实现:UIViewController 遵循 GreetingViewInputProtocol
class GreetingViewController: UIViewController {
// MARK: - UI 组件
private let nameTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "输入你的名字"
tf.borderStyle = .roundedRect
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
private let generateButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("生成问候", for: .normal)
btn.translatesAutoresizingMaskIntoConstraints = false
return btn
}()
private let resultLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// 持有Presenter的引用(通过协议)
var presenter: GreetingViewOutputProtocol?
// MARK: - 生命周期
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupActions()
}
private func setupUI() {
view.backgroundColor = .white
view.addSubview(nameTextField)
view.addSubview(generateButton)
view.addSubview(resultLabel)
NSLayoutConstraint.activate([
nameTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
nameTextField.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50),
nameTextField.widthAnchor.constraint(equalToConstant: 200),
generateButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
generateButton.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20),
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.topAnchor.constraint(equalTo: generateButton.bottomAnchor, constant: 30),
resultLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
resultLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func setupActions() {
generateButton.addTarget(self, action: #selector(generateButtonTapped), for: .touchUpInside)
}
@objc private func generateButtonTapped() {
// 用户点击按钮,将事件传递给Presenter
presenter?.didTapGenerateButton(with: nameTextField.text)
}
}
// MARK: - GreetingViewInputProtocol
extension GreetingViewController: GreetingViewInputProtocol {
func showGreeting(_ message: String) {
resultLabel.text = message
resultLabel.textColor = .systemGreen
}
func showError(_ error: String) {
resultLabel.text = error
resultLabel.textColor = .systemRed
}
}
5. Router(路由器)
Router 负责模块间的导航和模块创建。本示例中虽然没有跳转到其他模块,但 Router 仍然负责返回当前视图控制器,或者构建模块。
// GreetingRouterProtocol.swift
import Foundation
import UIKit
/// 路由器协议
protocol GreetingRouterProtocol: AnyObject {
/// 返回当前模块的入口视图控制器(用于构建时)
static func createModule() -> UIViewController
/// 如果需要跳转到其他模块,在这里添加方法,例如:
// func navigateToDetail(from view: GreetingViewInputProtocol?)
}
// GreetingRouter.swift
import UIKit
/// 路由器实现
class GreetingRouter: GreetingRouterProtocol {
// MARK: - 模块构建
static func createModule() -> UIViewController {
// 创建所有组件
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
let router = GreetingRouter() // 注意:这里没有强引用,但Router本身不需要持有其他组件
// 建立连接
view.presenter = presenter
presenter.view = view
presenter.interactor = interactor
presenter.router = router
interactor.presenter = presenter
return view
}
// 如果需要导航,可以实现类似下面的方法:
/*
func navigateToDetail(from view: GreetingViewInputProtocol?) {
guard let sourceView = view as? UIViewController else { return }
let detailVC = DetailRouter.createModule()
sourceView.navigationController?.pushViewController(detailVC, animated: true)
}
*/
}
6. 模块组装
上面的GreetingRouter.createModule()已经完成了模块的组装。在 App 启动或需要显示该模块的地方,直接调用:
// 例如在 AppDelegate 或 SceneDelegate 中:
let greetingVC = GreetingRouter.createModule()
window?.rootViewController = greetingVC
三、VIPER 工作流程说明
-
用户操作:用户在
GreetingViewController的文本框输入名字,点击按钮。 -
View -> Presenter:按钮事件调用
presenter?.didTapGenerateButton(with: nameTextField.text)。 -
Presenter -> Interactor:Presenter 调用
interactor?.generateGreeting(for: name)。 - Interactor 业务处理:Interactor 检查名字是否为空,拼接欢迎语,然后通过输出协议回调 Presenter。
-
Interactor -> Presenter:
interactor.presenter?.didGenerateGreeting(message)或didFailToGenerateGreeting。 - Presenter 格式化数据:Presenter 直接传递结果给 View(这里没有复杂格式化)。
-
Presenter -> View:
view?.showGreeting(message)或showError(error)。 - View 更新 UI:ViewController 更新 resultLabel 的文本和颜色。
如果涉及导航,Presenter 会调用router.navigateToDetail(from: view),Router 执行实际跳转。
四、为什么 VIPER 职责分离更彻底?
- View:只负责 UI 布局和用户事件传递,不处理业务逻辑。
- Interactor:只负责业务逻辑(如数据验证、字符串拼接),不涉及 UI。
- Presenter:只负责协调 View 和 Interactor,将业务结果转换为 UI 可展示的格式,不处理具体业务和导航。
- Router:只负责导航和模块创建。
- Entity:纯数据模型。
每个组件通过协议(接口)通信,依赖关系清晰且单向,替换或测试任何一个组件都不会影响其他组件。
五、专家进阶:处理更复杂的场景
上面的例子很好地展示了核心概念,但实际项目远比这复杂。我们来看看如何处理一些常见场景。
1. 引入 Service 层处理网络请求
当业务涉及网络或数据库时,直接在 Interactor 中处理会使它变得臃肿。我们可以引入 Service 或 Repository 层。
职责变化:
- Interactor:编排业务流,决定何时调用 Service,并处理 Service 返回的结果(成功/失败),但它本身不关心数据是从网络还是本地来的。
- Service/Repository:负责具体的数据获取(网络、缓存、数据库)。
结构示例:
Interactor (GreetingInteractor) → 调用 → GreetingServiceProtocol (由具体服务如 GreetingAPIService 实现)
↑
(回调结果)
Interactor 依赖 Service 的协议,使得服务层也可以轻松替换和测试。
2. 模块间数据传递
模块 A (列表) 跳转到 模块 B (详情) 时,需要传递数据(如用户ID)。
推荐做法:
在 Router 的方法中携带参数。例如:
// 在 ModuleA 的 Router 协议中
func navigateToDetail(from view: ModuleAViewInputProtocol?, with userId: String)
// 在 Router 实现中
func navigateToDetail(from view: ModuleAViewInputProtocol?, with userId: String) {
guard let sourceView = view as? UIViewController else { return }
// 通过 ModuleB 的 Builder/Router 创建模块并传入 userId
let detailVC = ModuleBRouter.createModule(with: userId)
sourceView.navigationController?.pushViewController(detailVC, animated: true)
}
这样,数据传递是显式的,类型安全。
3. 使用 Result 类型优化回调
Swift 的 Result<Success, Failure> 枚举可以让回调更统一、更Swift化。
改造 Interactor 输出协议:
protocol GreetingInteractorOutputProtocol: AnyObject {
// 将两个方法合并为一个
func didGenerateGreeting(result: Result<String, Error>)
}
// Interactor 内部调用
presenter?.didGenerateGreeting(result: .success(message))
// 或
presenter?.didGenerateGreeting(result: .failure(ValidationError.emptyName))
// Presenter 处理
func didGenerateGreeting(result: Result<String, Error>) {
switch result {
case .success(let message):
view?.showGreeting(message)
case .failure(let error):
view?.showError(error.localizedDescription)
}
}
4. 处理加载状态
UI 通常需要显示加载中(如转菊花)。View 协议可以增加状态方法:
protocol GreetingViewInputProtocol: AnyObject {
func showLoading()
func hideLoading()
// ... 其他方法
}
调用时机:
-
Presenter 在调用
interactor?.generateGreeting(...)之前,调用view?.showLoading()。 -
Presenter 在接收到 Interactor 的回调(无论是成功还是失败)之后,调用
view?.hideLoading()。
六、VIPER 最佳实践与常见陷阱
最佳实践
- 协议最小化:每个协议只定义该组件必需的方法,避免“万能协议”。
-
Presenter 无 UI 逻辑:不要在 Presenter 中导入
UIKit。所有与 UI 框架相关的操作(如UIColor、UIFont)都应该在 View 层处理,或由 Presenter 传递“描述性”数据(例如一个枚举值.success,View 根据它设置颜色)。 - Interactor 无状态:理想情况下,Interactor 应该是无状态的,它根据输入执行操作并返回结果。状态应该由 Entity 或更上层的 Service 管理。
-
Router 是唯一知道 UIKit 导航细节的地方:例如,是
push还是present,这些代码都应该封装在 Router 中。 -
使用构建者(Builder)模式:用一个独立的 Builder(如
GreetingModuleBuilder)来组装模块,而不是放在 Router 的静态方法里,职责更单一。
常见陷阱
-
循环引用:
- View 强持有 Presenter?否。View 应弱引用 Presenter(或通过协议持有,协议本身是引用类型,需用
weak)。 - Presenter 强持有 View、Interactor、Router?是,Presenter 需要协调它们,所以可以强持有。
- Interactor 强持有 Presenter?否。Interactor 的输出代理 (
presenter) 必须是weak,否则会造成 Presenter ↔ Interactor 循环引用。
- View 强持有 Presenter?否。View 应弱引用 Presenter(或通过协议持有,协议本身是引用类型,需用
- 过度设计:对于极其简单的页面(如一个静态设置项),VIPER 可能显得笨重。可以酌情使用 MVC 或 MVVM,但保持模块边界清晰。
- 臃肿的 Presenter:如果发现 Presenter 代码过多,可能是业务逻辑没有下放到 Interactor,或者 UI 格式化逻辑没有在 View 层处理。考虑将一些辅助逻辑抽取到 helpers 或 view models 中。
七、与 MVVM 的对比
| 特性 | MVVM | VIPER |
|---|---|---|
| 组件 | View, ViewModel, Model | View, Presenter, Interactor, Router, Entity |
| 职责边界 | 较清晰 | 极其清晰,每个组件单一职责 |
| 业务逻辑层 | 通常在 Model 或 Service | 明确的 Interactor 层,独立且可测试 |
| 导航逻辑 | 通常在 View (通过 Coordinator) 或 ViewModel | 明确的 Router 层,集中管理 |
| 学习曲线 | 平缓 | 陡峭 |
| 测试性 | 好(ViewModel 易于测试) | 极佳(每个组件都可独立单元测试) |
| 适用场景 | 中小型项目,或从 MVC 过渡 | 大型项目,复杂业务,多人协作,对可维护性和测试性要求极高的项目 |
选择建议:如果你的项目逻辑复杂,需要长期维护和大量测试,VIPER 是值得投资的。如果是快速原型或简单应用,MVVM 可能是更高效的选择。
八、测试示例(完整版)
原文中的测试示例很好,我们基于 Result 类型和 mock 对象,让它更健壮。
// 测试 Presenter
func testGenerateGreetingSuccess() {
// 1. Given (准备)
let presenter = GreetingPresenter()
let mockView = MockGreetingView()
let mockInteractor = MockGreetingInteractor()
presenter.view = mockView
presenter.interactor = mockInteractor
mockInteractor.presenter = presenter
let expectedName = "李四"
// 2. When (执行)
presenter.didTapGenerateButton(with: expectedName)
// 3. Then (验证)
// 验证 Interactor 被正确调用
XCTAssertEqual(mockInteractor.receivedName, expectedName)
XCTAssertTrue(mockInteractor.generateGreetingCalled)
// 模拟 Interactor 成功回调
mockInteractor.presenter?.didGenerateGreeting(result: .success("你好,李四!"))
// 验证 View 被正确更新
XCTAssertEqual(mockView.displayedMessage, "你好,李四!")
XCTAssertFalse(mockView.displayedErrorCalled) // 假设 MockView 能记录错误方法是否被调用
}
// Mock 对象示例
class MockGreetingInteractor: GreetingInteractorInputProtocol {
weak var presenter: GreetingInteractorOutputProtocol?
var generateGreetingCalled = false
var receivedName: String?
func generateGreeting(for name: String) {
generateGreetingCalled = true
receivedName = name
}
}
class MockGreetingView: GreetingViewInputProtocol {
var displayedMessage: String?
var displayedError: String?
var displayedErrorCalled = false
func showGreeting(_ message: String) {
displayedMessage = message
}
func showError(_ error: String) {
displayedError = error
displayedErrorCalled = true
}
}
九、总结
这个简单的欢迎模块完整展示了 VIPER 的五个组件及其协作方式。虽然比 MVC 多写了一些代码,但带来的可测试性、可维护性和职责清晰度在大型项目中非常宝贵。初级开发者可以从这个示例开始,逐步理解 VIPER 的设计思想,并应用到更复杂的场景中。
最后编辑于:2026-03-05