架构:VIPER职责分离更彻底

一、VIPER组件关系图与核心解读

以下是VIPER组件关系图:


ddca4c7be70263d71c50b4e16c4b4590.png

图表解读

  • 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 -> Presenterinteractor.presenter?.didGenerateGreeting(message)didFailToGenerateGreeting
  • Presenter 格式化数据:Presenter 直接传递结果给 View(这里没有复杂格式化)。
  • Presenter -> Viewview?.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 中处理会使它变得臃肿。我们可以引入 ServiceRepository 层。

职责变化

  • 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 最佳实践与常见陷阱

最佳实践

  1. 协议最小化:每个协议只定义该组件必需的方法,避免“万能协议”。
  2. Presenter 无 UI 逻辑:不要在 Presenter 中导入 UIKit。所有与 UI 框架相关的操作(如 UIColorUIFont)都应该在 View 层处理,或由 Presenter 传递“描述性”数据(例如一个枚举值 .success,View 根据它设置颜色)。
  3. Interactor 无状态:理想情况下,Interactor 应该是无状态的,它根据输入执行操作并返回结果。状态应该由 Entity 或更上层的 Service 管理。
  4. Router 是唯一知道 UIKit 导航细节的地方:例如,是 push 还是 present,这些代码都应该封装在 Router 中。
  5. 使用构建者(Builder)模式:用一个独立的 Builder(如 GreetingModuleBuilder)来组装模块,而不是放在 Router 的静态方法里,职责更单一。

常见陷阱

  1. 循环引用
    • View 强持有 Presenter?。View 应弱引用 Presenter(或通过协议持有,协议本身是引用类型,需用 weak)。
    • Presenter 强持有 View、Interactor、Router?,Presenter 需要协调它们,所以可以强持有。
    • Interactor 强持有 Presenter?。Interactor 的输出代理 (presenter) 必须是 weak,否则会造成 Presenter ↔ Interactor 循环引用。
  2. 过度设计:对于极其简单的页面(如一个静态设置项),VIPER 可能显得笨重。可以酌情使用 MVC 或 MVVM,但保持模块边界清晰。
  3. 臃肿的 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容