vip-clean-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

VIP Clean Architecture

VIP 整洁架构

Overview

概述

VIP (View-Interactor-Presenter) is Uncle Bob's Clean Architecture applied to iOS with strict unidirectional data flow. Unlike MVVM (bidirectional) or VIPER (complex 5-component), VIP uses 3 core components with protocol-based boundaries.
Core Principle: Data flows in one direction: View → Interactor → Presenter → View. No component ever calls backward in the cycle.
VIP(View-Interactor-Presenter)是将Uncle Bob的整洁架构应用于iOS的实现,采用严格的单向数据流。与MVVM(双向绑定)或VIPER(5个复杂组件)不同,VIP使用3个核心组件,并基于协议定义边界。
核心原则: 数据仅沿一个方向流动:View → Interactor → Presenter → View。任何组件都不得反向调用。

When to Use VIP

何时使用VIP

dot
digraph vip_decision {
    "Enterprise app?" [shape=diamond];
    "Max testability?" [shape=diamond];
    "Complex business logic?" [shape=diamond];
    "Team > 3 devs?" [shape=diamond];
    "Use VIP" [shape=box, style=filled, fillcolor=lightgreen];
    "Use MVVM" [shape=box, style=filled, fillcolor=lightblue];

    "Enterprise app?" -> "Max testability?" [label="yes"];
    "Enterprise app?" -> "Use MVVM" [label="no"];
    "Max testability?" -> "Use VIP" [label="yes"];
    "Max testability?" -> "Complex business logic?" [label="no"];
    "Complex business logic?" -> "Team > 3 devs?" [label="yes"];
    "Complex business logic?" -> "Use MVVM" [label="no"];
    "Team > 3 devs?" -> "Use VIP" [label="yes"];
    "Team > 3 devs?" -> "Use MVVM" [label="no"];
}
Use VIP for:
  • Enterprise iOS apps with strict quality requirements
  • Features requiring 80%+ test coverage
  • Complex business logic with multiple edge cases
  • Multi-team projects needing clear boundaries
  • Apps where testability > development speed
Use MVVM instead for:
  • Standard iOS apps (most apps)
  • Rapid prototyping or MVPs
  • Simple CRUD applications
  • Small teams (1-2 developers)
  • Projects prioritizing delivery speed
dot
digraph vip_decision {
    "Enterprise app?" [shape=diamond];
    "Max testability?" [shape=diamond];
    "Complex business logic?" [shape=diamond];
    "Team > 3 devs?" [shape=diamond];
    "Use VIP" [shape=box, style=filled, fillcolor=lightgreen];
    "Use MVVM" [shape=box, style=filled, fillcolor=lightblue];

    "Enterprise app?" -> "Max testability?" [label="yes"];
    "Enterprise app?" -> "Use MVVM" [label="no"];
    "Max testability?" -> "Use VIP" [label="yes"];
    "Max testability?" -> "Complex business logic?" [label="no"];
    "Complex business logic?" -> "Team > 3 devs?" [label="yes"];
    "Complex business logic?" -> "Use MVVM" [label="no"];
    "Team > 3 devs?" -> "Use VIP" [label="yes"];
    "Team > 3 devs?" -> "Use MVVM" [label="no"];
}
适合使用VIP的场景:
  • 有严格质量要求的企业级iOS应用
  • 需要80%以上测试覆盖率的功能
  • 包含多个边缘情况的复杂业务逻辑
  • 需要清晰边界的多团队项目
  • 优先考虑可测试性而非开发速度的应用
适合使用MVVM的场景:
  • 标准iOS应用(大多数应用)
  • 快速原型开发或MVP
  • 简单CRUD应用
  • 小型团队(1-2名开发者)
  • 优先考虑交付速度的项目

VIP Components

VIP组件

ComponentResponsibilityTestabilityWhat It Must NOT Do
ViewDisplay, user input, lifecycle eventsUI tests onlyBusiness logic, formatting, navigation
InteractorBusiness logic, use cases, orchestration100% unit testableUI updates, data formatting, navigation
PresenterFormat data for display, prepare view models100% unit testableBusiness logic, network calls, persistence
WorkerExternal services (network, DB, APIs)Mock/stub in testsBusiness logic, data formatting
RouterNavigation, screen transitionsIntegration testsBusiness logic, data management
组件职责可测试性禁止操作
View展示UI、处理用户输入、生命周期事件仅可进行UI测试业务逻辑、数据格式化、页面导航
Interactor业务逻辑、用例实现、流程编排100%可单元测试UI更新、数据格式化、页面导航
Presenter为展示格式化数据、准备视图模型100%可单元测试业务逻辑、网络请求、数据持久化
Worker外部服务(网络、数据库、API)测试中使用Mock/Stub业务逻辑、数据格式化
Router页面导航、屏幕转场集成测试业务逻辑、数据管理

VIP Data Flow (Critical)

VIP数据流(核心规则)

Rule: Data flows in ONE direction only. Never call backward in the cycle.
User Action → View → Interactor → Presenter → View
                ↑                              ↓
                └──────── Display ─────────────┘
Detailed Flow:
  1. View captures user action → calls
    Interactor.doSomething(request: Request)
  2. Interactor executes business logic → calls
    Presenter.presentSomething(response: Response)
  3. Presenter formats data → calls
    View.displaySomething(viewModel: ViewModel)
  4. View updates UI with formatted data
Example: Login Flow
User taps "Login" button
→ View calls interactor.login(request: LoginRequest(email: "...", password: "..."))
→ Interactor validates input, calls Worker.authenticate(...)
→ Worker makes API call, returns Result<User, Error>
→ Interactor processes result, calls presenter.presentLoginResult(response: LoginResponse.success(user))
→ Presenter formats: "Welcome, John!" → calls view.displayWelcome(viewModel: LoginViewModel.success(message: "Welcome, John!"))
→ View updates UILabel.text = "Welcome, John!"
规则: 数据仅沿一个方向流动。绝对不能反向调用。
用户操作 → View → Interactor → Presenter → View
                ↑                              ↓
                └────────── 展示UI ────────────┘
详细流程:
  1. View捕获用户操作 → 调用
    Interactor.doSomething(request: Request)
  2. Interactor执行业务逻辑 → 调用
    Presenter.presentSomething(response: Response)
  3. Presenter格式化数据 → 调用
    View.displaySomething(viewModel: ViewModel)
  4. View使用格式化后的数据更新UI
示例:登录流程
用户点击“登录”按钮
→ View调用interactor.login(request: LoginRequest(email: "...", password: "..."))
→ Interactor验证输入,调用Worker.authenticate(...)
→ Worker发起API请求,返回Result<User, Error>
→ Interactor处理结果,调用presenter.presentLoginResult(response: LoginResponse.success(user))
→ Presenter格式化内容:"Welcome, John!" → 调用view.displayWelcome(viewModel: LoginViewModel.success(message: "Welcome, John!"))
→ View更新UILabel.text = "Welcome, John!"

Protocol-Based Communication

基于协议的通信

Critical Rule: All components communicate via protocols, never concrete types.
swift
// MARK: - Protocols (VIP Cycle)

protocol LoginBusinessLogic {
    func login(request: LoginRequest)
}

protocol LoginPresentationLogic {
    func presentLoginResult(response: LoginResponse)
}

protocol LoginDisplayLogic: AnyObject {
    func displayWelcome(viewModel: LoginViewModel)
    func displayError(viewModel: LoginViewModel)
}

// MARK: - Data Models (Request → Response → ViewModel)

struct LoginRequest {
    let email: String
    let password: String
}

enum LoginResponse {
    case success(user: User)
    case failure(error: Error)
}

enum LoginViewModel {
    case success(message: String)
    case error(title: String, message: String)
}

// MARK: - View

final class LoginViewController: UIViewController {
    var interactor: LoginBusinessLogic?
    var router: LoginRoutingLogic?

    @IBAction func loginButtonTapped() {
        let request = LoginRequest(
            email: emailTextField.text ?? "",
            password: passwordTextField.text ?? ""
        )
        interactor?.login(request: request)
    }
}

extension LoginViewController: LoginDisplayLogic {
    func displayWelcome(viewModel: LoginViewModel) {
        guard case .success(let message) = viewModel else { return }
        welcomeLabel.text = message
        router?.routeToHome()
    }

    func displayError(viewModel: LoginViewModel) {
        guard case .error(let title, let message) = viewModel else { return }
        showAlert(title: title, message: message)
    }
}

// MARK: - Interactor

final class LoginInteractor: LoginBusinessLogic {
    var presenter: LoginPresentationLogic?
    var worker: LoginWorkerProtocol?

    func login(request: LoginRequest) {
        // Validation (business logic)
        guard !request.email.isEmpty, !request.password.isEmpty else {
            presenter?.presentLoginResult(response: .failure(error: ValidationError.emptyFields))
            return
        }

        // Delegate to Worker for external service
        worker?.authenticate(email: request.email, password: request.password) { [weak self] result in
            switch result {
            case .success(let user):
                self?.presenter?.presentLoginResult(response: .success(user: user))
            case .failure(let error):
                self?.presenter?.presentLoginResult(response: .failure(error: error))
            }
        }
    }
}

// MARK: - Presenter

final class LoginPresenter: LoginPresentationLogic {
    weak var viewController: LoginDisplayLogic?

    func presentLoginResult(response: LoginResponse) {
        switch response {
        case .success(let user):
            let viewModel = LoginViewModel.success(message: "Welcome, \(user.name)!")
            viewController?.displayWelcome(viewModel: viewModel)

        case .failure(let error):
            let viewModel = LoginViewModel.error(
                title: "Login Failed",
                message: error.localizedDescription
            )
            viewController?.displayError(viewModel: viewModel)
        }
    }
}

// MARK: - Worker

protocol LoginWorkerProtocol {
    func authenticate(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void)
}

final class LoginWorker: LoginWorkerProtocol {
    func authenticate(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
        // Network call, Core Data fetch, or external API
        APIClient.shared.login(email: email, password: password, completion: completion)
    }
}
核心规则: 所有组件通过协议通信,绝不直接依赖具体类型。
swift
// MARK: - Protocols (VIP Cycle)

protocol LoginBusinessLogic {
    func login(request: LoginRequest)
}

protocol LoginPresentationLogic {
    func presentLoginResult(response: LoginResponse)
}

protocol LoginDisplayLogic: AnyObject {
    func displayWelcome(viewModel: LoginViewModel)
    func displayError(viewModel: LoginViewModel)
}

// MARK: - Data Models (Request → Response → ViewModel)

struct LoginRequest {
    let email: String
    let password: String
}

enum LoginResponse {
    case success(user: User)
    case failure(error: Error)
}

enum LoginViewModel {
    case success(message: String)
    case error(title: String, message: String)
}

// MARK: - View

final class LoginViewController: UIViewController {
    var interactor: LoginBusinessLogic?
    var router: LoginRoutingLogic?

    @IBAction func loginButtonTapped() {
        let request = LoginRequest(
            email: emailTextField.text ?? "",
            password: passwordTextField.text ?? ""
        )
        interactor?.login(request: request)
    }
}

extension LoginViewController: LoginDisplayLogic {
    func displayWelcome(viewModel: LoginViewModel) {
        guard case .success(let message) = viewModel else { return }
        welcomeLabel.text = message
        router?.routeToHome()
    }

    func displayError(viewModel: LoginViewModel) {
        guard case .error(let title, let message) = viewModel else { return }
        showAlert(title: title, message: message)
    }
}

// MARK: - Interactor

final class LoginInteractor: LoginBusinessLogic {
    var presenter: LoginPresentationLogic?
    var worker: LoginWorkerProtocol?

    func login(request: LoginRequest) {
        // 验证(业务逻辑)
        guard !request.email.isEmpty, !request.password.isEmpty else {
            presenter?.presentLoginResult(response: .failure(error: ValidationError.emptyFields))
            return
        }

        // 委托Worker处理外部服务
        worker?.authenticate(email: request.email, password: request.password) { [weak self] result in
            switch result {
            case .success(let user):
                self?.presenter?.presentLoginResult(response: .success(user: user))
            case .failure(let error):
                self?.presenter?.presentLoginResult(response: .failure(error: error))
            }
        }
    }
}

// MARK: - Presenter

final class LoginPresenter: LoginPresentationLogic {
    weak var viewController: LoginDisplayLogic?

    func presentLoginResult(response: LoginResponse) {
        switch response {
        case .success(let user):
            let viewModel = LoginViewModel.success(message: "Welcome, \(user.name)!")
            viewController?.displayWelcome(viewModel: viewModel)

        case .failure(let error):
            let viewModel = LoginViewModel.error(
                title: "Login Failed",
                message: error.localizedDescription
            )
            viewController?.displayError(viewModel: viewModel)
        }
    }
}

// MARK: - Worker

protocol LoginWorkerProtocol {
    func authenticate(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void)
}

final class LoginWorker: LoginWorkerProtocol {
    func authenticate(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
        // 网络请求、Core Data查询或外部API调用
        APIClient.shared.login(email: email, password: password, completion: completion)
    }
}

VIP Testing Strategy (Spy Pattern)

VIP测试策略(Spy模式)

Critical Rule: Use Spy objects to verify protocol method calls, not XCTest assertions on properties.
核心规则: 使用Spy对象验证协议方法调用,而非对属性使用XCTest断言。

Why Spies over Mocks?

为什么选择Spy而非Mock?

  • Spies record calls: Verify that correct methods were called with correct parameters
  • Mocks return data: Provide predetermined responses for testing
  • VIP needs Spies: We test protocol contracts, not implementation details
  • Spy记录调用: 验证是否调用了正确的方法并传入了正确的参数
  • Mock返回数据: 为测试提供预设响应
  • VIP需要Spy: 我们测试的是协议契约,而非实现细节

Testing the Interactor

测试Interactor

swift
final class LoginInteractorTests: XCTestCase {
    var sut: LoginInteractor!
    var presenterSpy: LoginPresenterSpy!
    var workerSpy: LoginWorkerSpy!

    override func setUp() {
        super.setUp()
        sut = LoginInteractor()
        presenterSpy = LoginPresenterSpy()
        workerSpy = LoginWorkerSpy()
        sut.presenter = presenterSpy
        sut.worker = workerSpy
    }

    func testLoginWithValidCredentialsCallsWorker() {
        // Given
        let request = LoginRequest(email: "test@example.com", password: "password123")

        // When
        sut.login(request: request)

        // Then
        XCTAssertTrue(workerSpy.authenticateCalled)
        XCTAssertEqual(workerSpy.authenticateEmail, "test@example.com")
        XCTAssertEqual(workerSpy.authenticatePassword, "password123")
    }

    func testLoginWithEmptyEmailPresentsError() {
        // Given
        let request = LoginRequest(email: "", password: "password123")

        // When
        sut.login(request: request)

        // Then
        XCTAssertTrue(presenterSpy.presentLoginResultCalled)
        if case .failure(let error) = presenterSpy.presentLoginResultResponse {
            XCTAssertTrue(error is ValidationError)
        } else {
            XCTFail("Expected failure response")
        }
    }
}

// MARK: - Presenter Spy

final class LoginPresenterSpy: LoginPresentationLogic {
    var presentLoginResultCalled = false
    var presentLoginResultResponse: LoginResponse?

    func presentLoginResult(response: LoginResponse) {
        presentLoginResultCalled = true
        presentLoginResultResponse = response
    }
}

// MARK: - Worker Spy

final class LoginWorkerSpy: LoginWorkerProtocol {
    var authenticateCalled = false
    var authenticateEmail: String?
    var authenticatePassword: String?
    var authenticateResult: Result<User, Error> = .success(User(id: "1", name: "Test User"))

    func authenticate(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
        authenticateCalled = true
        authenticateEmail = email
        authenticatePassword = password
        completion(authenticateResult)
    }
}
swift
final class LoginInteractorTests: XCTestCase {
    var sut: LoginInteractor!
    var presenterSpy: LoginPresenterSpy!
    var workerSpy: LoginWorkerSpy!

    override func setUp() {
        super.setUp()
        sut = LoginInteractor()
        presenterSpy = LoginPresenterSpy()
        workerSpy = LoginWorkerSpy()
        sut.presenter = presenterSpy
        sut.worker = workerSpy
    }

    func testLoginWithValidCredentialsCallsWorker() {
        // 给定
        let request = LoginRequest(email: "test@example.com", password: "password123")

        // 执行
        sut.login(request: request)

        // 断言
        XCTAssertTrue(workerSpy.authenticateCalled)
        XCTAssertEqual(workerSpy.authenticateEmail, "test@example.com")
        XCTAssertEqual(workerSpy.authenticatePassword, "password123")
    }

    func testLoginWithEmptyEmailPresentsError() {
        // 给定
        let request = LoginRequest(email: "", password: "password123")

        // 执行
        sut.login(request: request)

        // 断言
        XCTAssertTrue(presenterSpy.presentLoginResultCalled)
        if case .failure(let error) = presenterSpy.presentLoginResultResponse {
            XCTAssertTrue(error is ValidationError)
        } else {
            XCTFail("预期失败响应")
        }
    }
}

// MARK: - Presenter Spy

final class LoginPresenterSpy: LoginPresentationLogic {
    var presentLoginResultCalled = false
    var presentLoginResultResponse: LoginResponse?

    func presentLoginResult(response: LoginResponse) {
        presentLoginResultCalled = true
        presentLoginResultResponse = response
    }
}

// MARK: - Worker Spy

final class LoginWorkerSpy: LoginWorkerProtocol {
    var authenticateCalled = false
    var authenticateEmail: String?
    var authenticatePassword: String?
    var authenticateResult: Result<User, Error> = .success(User(id: "1", name: "Test User"))

    func authenticate(email: String, password: String, completion: @escaping (Result<User, Error>) -> Void) {
        authenticateCalled = true
        authenticateEmail = email
        authenticatePassword = password
        completion(authenticateResult)
    }
}

Testing the Presenter

测试Presenter

swift
final class LoginPresenterTests: XCTestCase {
    var sut: LoginPresenter!
    var viewControllerSpy: LoginViewControllerSpy!

    override func setUp() {
        super.setUp()
        sut = LoginPresenter()
        viewControllerSpy = LoginViewControllerSpy()
        sut.viewController = viewControllerSpy
    }

    func testPresentLoginSuccessFormatsWelcomeMessage() {
        // Given
        let user = User(id: "1", name: "John Doe")
        let response = LoginResponse.success(user: user)

        // When
        sut.presentLoginResult(response: response)

        // Then
        XCTAssertTrue(viewControllerSpy.displayWelcomeCalled)
        if case .success(let message) = viewControllerSpy.displayWelcomeViewModel {
            XCTAssertEqual(message, "Welcome, John Doe!")
        } else {
            XCTFail("Expected success viewModel")
        }
    }

    func testPresentLoginFailureFormatsErrorMessage() {
        // Given
        let error = NSError(domain: "Test", code: 401, userInfo: [NSLocalizedDescriptionKey: "Invalid credentials"])
        let response = LoginResponse.failure(error: error)

        // When
        sut.presentLoginResult(response: response)

        // Then
        XCTAssertTrue(viewControllerSpy.displayErrorCalled)
        if case .error(let title, let message) = viewControllerSpy.displayErrorViewModel {
            XCTAssertEqual(title, "Login Failed")
            XCTAssertEqual(message, "Invalid credentials")
        } else {
            XCTFail("Expected error viewModel")
        }
    }
}

// MARK: - View Spy

final class LoginViewControllerSpy: LoginDisplayLogic {
    var displayWelcomeCalled = false
    var displayWelcomeViewModel: LoginViewModel?

    var displayErrorCalled = false
    var displayErrorViewModel: LoginViewModel?

    func displayWelcome(viewModel: LoginViewModel) {
        displayWelcomeCalled = true
        displayWelcomeViewModel = viewModel
    }

    func displayError(viewModel: LoginViewModel) {
        displayErrorCalled = true
        displayErrorViewModel = viewModel
    }
}
swift
final class LoginPresenterTests: XCTestCase {
    var sut: LoginPresenter!
    var viewControllerSpy: LoginViewControllerSpy!

    override func setUp() {
        super.setUp()
        sut = LoginPresenter()
        viewControllerSpy = LoginViewControllerSpy()
        sut.viewController = viewControllerSpy
    }

    func testPresentLoginSuccessFormatsWelcomeMessage() {
        // 给定
        let user = User(id: "1", name: "John Doe")
        let response = LoginResponse.success(user: user)

        // 执行
        sut.presentLoginResult(response: response)

        // 断言
        XCTAssertTrue(viewControllerSpy.displayWelcomeCalled)
        if case .success(let message) = viewControllerSpy.displayWelcomeViewModel {
            XCTAssertEqual(message, "Welcome, John Doe!")
        } else {
            XCTFail("预期成功视图模型")
        }
    }

    func testPresentLoginFailureFormatsErrorMessage() {
        // 给定
        let error = NSError(domain: "Test", code: 401, userInfo: [NSLocalizedDescriptionKey: "Invalid credentials"])
        let response = LoginResponse.failure(error: error)

        // 执行
        sut.presentLoginResult(response: response)

        // 断言
        XCTAssertTrue(viewControllerSpy.displayErrorCalled)
        if case .error(let title, let message) = viewControllerSpy.displayErrorViewModel {
            XCTAssertEqual(title, "Login Failed")
            XCTAssertEqual(message, "Invalid credentials")
        } else {
            XCTFail("预期错误视图模型")
        }
    }
}

// MARK: - View Spy

final class LoginViewControllerSpy: LoginDisplayLogic {
    var displayWelcomeCalled = false
    var displayWelcomeViewModel: LoginViewModel?

    var displayErrorCalled = false
    var displayErrorViewModel: LoginViewModel?

    func displayWelcome(viewModel: LoginViewModel) {
        displayWelcomeCalled = true
        displayWelcomeViewModel = viewModel
    }

    func displayError(viewModel: LoginViewModel) {
        displayErrorCalled = true
        displayErrorViewModel = viewModel
    }
}

Critical Rules

核心规则

✅ DO

✅ 必须遵守

  • Protocol everything: All component communication via protocols
  • Unidirectional flow: View → Interactor → Presenter → View (never backward)
  • Three data models: Request (View→Interactor), Response (Interactor→Presenter), ViewModel (Presenter→View)
  • Spy-based tests: Verify protocol method calls, not property assertions
  • Worker isolation: All external services (network, DB, location) in Workers
  • Presenter formats only: Strings, dates, colors, numbers prepared for display
  • 所有组件基于协议: 所有组件通信都通过协议
  • 单向数据流: View → Interactor → Presenter → View(绝不反向)
  • 三种数据模型: Request(View→Interactor)、Response(Interactor→Presenter)、ViewModel(Presenter→View)
  • 基于Spy的测试: 验证协议方法调用,而非属性断言
  • Worker隔离外部服务: 所有外部服务(网络、数据库、定位)都放在Worker中
  • 仅Presenter负责格式化: 字符串、日期、颜色、数字等都由Presenter准备用于展示

❌ DON'T

❌ 禁止操作

  • Never skip the cycle: View must NOT call Presenter directly
  • Never call backward: Presenter must NOT call Interactor
  • Never mix ViewModels: VIP uses Presenter, not ViewModel classes
  • Never use concrete types: Always depend on protocols
  • Never put business logic in Presenter: Business logic belongs in Interactor
  • Never put formatting in Interactor: Formatting belongs in Presenter
  • 绝不跳过流程: View不能直接调用Presenter
  • 绝不反向调用: Presenter不能调用Interactor
  • 绝不混用ViewModel: VIP使用Presenter,而非ViewModel类
  • 绝不依赖具体类型: 始终依赖协议
  • 绝不将业务逻辑放在Presenter: 业务逻辑属于Interactor
  • 绝不将格式化逻辑放在Interactor: 格式化属于Presenter

Anti-Patterns to Reject

需要避免的反模式

Anti-PatternWhy It's WrongCorrect Approach
View calls Presenter directlyBreaks unidirectional flow, bypasses business logicView always calls Interactor first
Presenter calls InteractorCreates circular dependency, breaks cycleInteractor calls Presenter, never reverse
Mixing ViewModel with VIPViewModel is MVVM concept, VIP uses PresenterRemove ViewModel, use Presenter for formatting
Business logic in PresenterPresenter should only format, not decideMove validation/logic to Interactor
Interactor updates ViewViolates separation, untestableInteractor → Presenter → View path
Using concrete typesHard to test, tight couplingAll components depend on protocols
反模式问题所在正确做法
View直接调用Presenter破坏单向数据流,绕过业务逻辑View必须先调用Interactor
Presenter调用Interactor产生循环依赖,破坏流程只能由Interactor调用Presenter,反之不行
VIP中混用ViewModelViewModel是MVVM的概念,VIP使用Presenter移除ViewModel,使用Presenter进行格式化
Presenter中包含业务逻辑Presenter应仅负责格式化,而非决策将验证/逻辑移至Interactor
Interactor直接更新View违反关注点分离,无法测试必须通过Interactor → Presenter → View的路径
依赖具体类型难以测试,耦合度高所有组件都依赖协议

VIP vs MVVM vs VIPER

VIP vs MVVM vs VIPER

AspectVIPMVVMVIPER
Components3 core (V-I-P) + Worker + Router2 (View + ViewModel)5 (V-I-P-E-R)
Data FlowUnidirectional cycleBidirectional bindingMulti-directional
Testability100% (protocol-based Spies)High (mock services)100% (protocol-based)
ComplexityMediumLowHigh
Best ForEnterprise apps, max testabilityMost iOS appsComplex multi-module apps
维度VIPMVVMVIPER
组件数量3个核心(V-I-P)+ Worker + Router2个(View + ViewModel)5个(V-I-P-E-R)
数据流单向循环双向绑定多向流动
可测试性100%(基于协议)高(Mock服务)100%(基于协议)
复杂度中等
最佳场景企业级应用、最高可测试性大多数iOS应用复杂多模块应用

Scene Assembly (Dependency Injection)

场景组装(依赖注入)

Configurator Pattern:
swift
final class LoginConfigurator {
    static func configure(_ viewController: LoginViewController) {
        let interactor = LoginInteractor()
        let presenter = LoginPresenter()
        let router = LoginRouter()
        let worker = LoginWorker()

        viewController.interactor = interactor
        viewController.router = router
        interactor.presenter = presenter
        interactor.worker = worker
        presenter.viewController = viewController
        router.viewController = viewController
    }
}

// Usage in AppDelegate or SceneDelegate
let loginVC = LoginViewController()
LoginConfigurator.configure(loginVC)
present(loginVC, animated: true)
配置器模式:
swift
final class LoginConfigurator {
    static func configure(_ viewController: LoginViewController) {
        let interactor = LoginInteractor()
        let presenter = LoginPresenter()
        let router = LoginRouter()
        let worker = LoginWorker()

        viewController.interactor = interactor
        viewController.router = router
        interactor.presenter = presenter
        interactor.worker = worker
        presenter.viewController = viewController
        router.viewController = viewController
    }
}

// 在AppDelegate或SceneDelegate中使用
let loginVC = LoginViewController()
LoginConfigurator.configure(loginVC)
present(loginVC, animated: true)

Migration from MVVM to VIP

从MVVM迁移到VIP

Step 1: Identify the ViewModel
swift
// Before (MVVM)
class LoginViewModel: ObservableObject {
    @Published var email = ""
    @Published var password = ""

    func login() async {
        // Business logic + formatting mixed
    }
}
Step 2: Split into Interactor (business logic) + Presenter (formatting)
swift
// After (VIP)

// Interactor: Business logic only
class LoginInteractor: LoginBusinessLogic {
    func login(request: LoginRequest) {
        // Validation logic
        // Call Worker
        // Pass raw response to Presenter
    }
}

// Presenter: Formatting only
class LoginPresenter: LoginPresentationLogic {
    func presentLoginResult(response: LoginResponse) {
        // Format user.name into "Welcome, John!"
        // Create ViewModel with formatted strings
    }
}
Step 3: Add protocol boundaries
swift
protocol LoginBusinessLogic { ... }
protocol LoginPresentationLogic { ... }
protocol LoginDisplayLogic: AnyObject { ... }
Step 4: Implement Spy-based tests
swift
class LoginPresenterSpy: LoginPresentationLogic { ... }
class LoginWorkerSpy: LoginWorkerProtocol { ... }
步骤1:识别ViewModel
swift
// 迁移前(MVVM)
class LoginViewModel: ObservableObject {
    @Published var email = ""
    @Published var password = ""

    func login() async {
        // 业务逻辑与格式化混合
    }
}
步骤2:拆分为Interactor(业务逻辑)+ Presenter(格式化)
swift
// 迁移后(VIP)

// Interactor:仅负责业务逻辑
class LoginInteractor: LoginBusinessLogic {
    func login(request: LoginRequest) {
        // 验证逻辑
        // 调用Worker
        // 将原始响应传递给Presenter
    }
}

// Presenter:仅负责格式化
class LoginPresenter: LoginPresentationLogic {
    func presentLoginResult(response: LoginResponse) {
        // 将user.name格式化为"Welcome, John!"
        // 创建包含格式化字符串的ViewModel
    }
}
步骤3:添加协议边界
swift
protocol LoginBusinessLogic { ... }
protocol LoginPresentationLogic { ... }
protocol LoginDisplayLogic: AnyObject { ... }
步骤4:实现基于Spy的测试
swift
class LoginPresenterSpy: LoginPresentationLogic { ... }
class LoginWorkerSpy: LoginWorkerProtocol { ... }

References

参考资料


Word count: ~2,100 For: Senior iOS engineers building enterprise apps Focus: Unidirectional flow, protocol-based testability, Spy pattern

字数统计: ~2,100 面向人群: 构建企业级应用的资深iOS工程师 核心重点: 单向数据流、基于协议的可测试性、Spy模式