iOS 架构模式全景图:MVC / MVVM / VIPER / Clean Architecture 选型指南

专栏 :iOS高级工程实践
编号 :C01 · 系列第 1 篇
字数 :约 6000 字
标签:iOS / 架构设计 / MVC / MVVM / VIPER / Clean Architecture / 架构选型


前言

「这个 App 架构选什么好?」

这是每个 iOS 工程师在项目初期都会面临的问题。网上有无数文章讲 MVC、MVVM、VIPER、Clean Architecture,但大多数只讲概念,不讲取舍。

今天,我们换一种方式:不只讲「是什么」,更讲「什么时候适合」,以及「换架构的代价是什么」。

读完这篇文章,你会得到:

  1. 决策树:根据项目特征快速匹配适合的架构
  2. 各架构的权衡分析:不只是优点,还有你必须付出的代价
  3. 渐进式迁移路径:如何从最简单的方式逐步演进到更复杂的架构

一、架构的本质是什么

在深入细节之前,我们需要先回答一个问题:架构到底是什么?

很多人把架构等同于「目录结构」或者「分层方式」,但这只是表面。

架构的本质是:系统中各组件之间 **关系**的定义。

这些关系包括:

  • 数据流向:数据从哪里来,经过哪些处理,流向哪里
  • 依赖方向:谁依赖谁,谁不应该知道谁的存在
  • 边界划分:哪些代码属于同一模块,模块之间如何通信
  • 变更隔离:当需求变化时,需要改多少代码,改哪些文件

好的架构让变更容易 ,坏的架构让变更痛苦


二、MVC:Apple 的默认选择

2.1 标准 MVC 在 iOS 中的表现

Apple 的 MVC(Model-View-Controller)模式大概是这个样子:

scss 复制代码
┌─────────┐     ┌─────────────┐     ┌─────────┐
│  View   │◀───▶│ Controller  │◀───▶│  Model  │
│(UIView) │     │(UIViewController)│   │ (数据) │
└─────────┘     └─────────────┘     └─────────┘

Model :纯数据和业务逻辑。不持有任何 UI 组件,不处理 UI 事件。 View :UI 组件(UIView / SwiftUI View)。负责渲染和用户交互的捕获。 Controller:协调者。接收 View 的事件,更新 Model,再把 Model 的变化反映到 View 上。

2.2 代码示例

swift 复制代码
// Model
struct User {
    let id: UUID
    var name: String
    var email: String
}

// Controller - 典型的 iOS MVC
class UserViewController: UIViewController {
    // Model
    var user: User?

    // View - 以 IBOutlet 形式存在
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var emailLabel: UILabel!
    @IBOutlet weak var saveButton: UIButton!

    // 生命周期
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupActions()
    }

    // 更新 View
    func updateUI() {
        nameLabel.text = user?.name
        emailLabel.text = user?.email
    }

    // 处理用户操作
    @IBAction func saveButtonTapped(_ sender: UIButton) {
        // 获取输入
        user?.name = nameTextField.text ?? ""
        user?.email = emailTextField.text ?? ""

        // 业务逻辑(可能在这里,也可能放在 Model 里)
        UserService.shared.save(user!) { result in
            DispatchQueue.main.async {
                switch result {
                case .success:
                    self.showSuccessAlert()
                case .failure(let error):
                    self.showErrorAlert(error)
                }
            }
        }
    }
}

2.3 MVC 的优点

  1. 简单直接:概念少,新人容易理解
  2. Apple 原生支持:Xcode 的模板默认就是 MVC
  3. 适合小型项目:对于功能简单的 App,MVC 的效率最高
  4. 代码量少:不需要额外的抽象层

2.4 MVC 的问题:Massive View Controller

MVC 最大的问题在于 Controller 的边界模糊。当业务逻辑复杂时,所有东西都会堆积到 Controller 里:

swift 复制代码
// 真实的 MVC Controller 会长这样
class OrderViewController: UIViewController {
    // 10+ 个 IBOutlet
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var totalLabel: UILabel!
    @IBOutlet weak var addressTextField: UITextField!
    @IBOutlet weak var paymentButton: UIButton!
    @IBOutlet weak var couponTextField: UITextField!
    // ... 更多 IBOutlet

    // 5+ 个数据属性
    var order: Order?
    var cartItems: [CartItem] = []
    var coupon: Coupon?
    var selectedAddress: Address?
    var paymentMethod: PaymentMethod = .alipay

    // 3+ 个服务依赖
    let orderService = OrderService()
    let paymentService = PaymentService()
    let couponService = CouponService()
    let analyticsService = AnalyticsService()
    let notificationService = NotificationService()

    // 网络请求回调嵌套
    func loadOrderDetail() {
        orderService.fetchOrder(id: orderId) { [weak self] result in
            self?.handleOrderFetch(result)
            self?.loadCouponList()
            self?.loadAddressList()
            self?.trackPageView()
        }
    }

    func applyCoupon() {
        couponService.validate(code: couponTextField.text) { [weak self] result in
            guard let self else { return }
            switch result {
            case .success(let coupon):
                self.coupon = coupon
                self.updateTotalPrice()
                self.showCouponSuccess()
            case .failure(let error):
                self.showCouponError(error)
                self.resetCoupon()
            }
        }
    }

    // 20+ 个方法
    func updateTotalPrice() { ... }
    func validateForm() { ... }
    func submitOrder() { ... }
    func handlePaymentResult() { ... }
    func sendLocalNotification() { ... }
    func logAnalytics() { ... }
    // ... 更多方法
}

这就是传说中的 Massive View Controller。当 Controller 超过 500 行,测试几乎不可能,任何修改都可能影响全局。

2.5 什么时候用 MVC

适合 不适合
小型 App(< 10 个页面) 中大型 App
功能相对固定 业务逻辑复杂多变
团队规模小(1-3 人) 团队规模大,需要并行开发
快速原型验证 需要长期维护
学习阶段 生产环境核心业务

三、MVVM:Apple 推荐的现代方案

3.1 MVVM 的核心思想

MVVM(Model-View-ViewModel)通过引入 ViewModel 来解决 Controller 膨胀的问题:

scss 复制代码
┌─────────┐     ┌───────────────┐     ┌─────────┐
│  View   │◀───▶│   ViewModel   │◀───▶│  Model  │
│(UIView) │     │ (状态+逻辑)    │     │ (数据)  │
└─────────┘     └───────────────┘     └─────────┘
       │                                  ▲
       └───────── 数据绑定 ───────────────┘

关键创新:View 和 ViewModel 之间的数据绑定。View 观察 ViewModel 的状态变化,自动更新 UI,不需要手动写「更新 UI」的代码。

3.2 iOS 中的数据绑定方案

方案 特点 学习曲线 性能
Combine Apple 原生,响应式流 中等
@Observable (iOS 17+) SwiftUI 原生,最简洁
RxSwift 功能最强大,社区成熟
SwiftUI @StateObject SwiftUI 内置

3.3 代码示例(Combine 版本)

swift 复制代码
import Combine

// ViewModel
class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var email: String = ""
    @Published var isLoading = false
    @Published var errorMessage: String?
    @Published var isSaveEnabled = false

    private var cancellables = Set<AnyCancellable>()
    private let userService: UserService

    init(userService: UserService = .shared) {
        self.userService = userService
        setupValidation()
    }

    // 业务逻辑在 ViewModel 中
    private func setupValidation() {
        // 组合多个验证条件
        Publishers.CombineLatest($name, $email)
            .map { name, email in
                !name.trimmingCharacters(in: .whitespaces).isEmpty &&
                email.contains("@") &&
                email.contains(".")
            }
            .assign(to: &$isSaveEnabled)
    }

    func save() async {
        isLoading = true
        errorMessage = nil

        do {
            let user = User(name: name, email: email)
            try await userService.save(user)
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }
}

// View(UIKit + Combine)
class UserViewController: UIViewController {
    private let viewModel = UserViewModel()
    private var cancellables = Set<AnyCancellable>()

    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var saveButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
        setupActions()
    }

    private func setupBindings() {
        // ViewModel → View:自动更新 UI
        viewModel.$isLoading
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isLoading in
                self?.saveButton.isEnabled = !isLoading
                self?.isLoading ? self?.showLoading() : self?.hideLoading()
            }
            .store(in: &cancellables)

        viewModel.$errorMessage
            .compactMap { $0 }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] message in
                self?.showError(message)
            }
            .store(in: &cancellables)

        viewModel.$isSaveEnabled
            .receive(on: DispatchQueue.main)
            .assign(to: \.isEnabled, on: saveButton)
            .store(in: &cancellables)
    }

    private func setupActions() {
        // View → ViewModel:用户输入驱动 ViewModel
        nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
        emailTextField.addTarget(self, action: #selector(emailChanged), for: .editingChanged)
        saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
    }

    @objc private func nameChanged(_ textField: UITextField) {
        viewModel.name = textField.text ?? ""
    }

    @objc private func emailChanged(_ textField: UITextField) {
        viewModel.email = textField.text ?? ""
    }

    @objc private func saveTapped() {
        Task {
            await viewModel.save()
        }
    }
}

3.4 SwiftUI 版本的 MVVM

swift 复制代码
import SwiftUI

// SwiftUI 天然 MVVM,数据绑定极简
struct UserView: View {
    @StateObject private var viewModel = UserViewModel()

    var body: some View {
        Form {
            TextField("Name", text: $viewModel.name)
            TextField("Email", text: $viewModel.email)

            Button("Save") {
                Task { await viewModel.save() }
            }
            .disabled(!viewModel.isSaveEnabled || viewModel.isLoading)

            if viewModel.isLoading {
                ProgressView()
            }

            if let error = viewModel.errorMessage {
                Text(error).foregroundColor(.red)
            }
        }
    }
}

3.5 MVVM 的优点

  1. 测试友好:ViewModel 不持有任何 UI 组件,可以直接测试
  2. Controller 瘦身:ViewController 从 500 行变成 50 行
  3. 数据绑定减少样板代码:不需要手动同步 UI 和数据
  4. 团队协作友好:View 和 ViewModel 可以并行开发

3.6 MVVM 的缺点

  1. 学习曲线:需要理解响应式编程或 Combine
  2. 过度工程风险:对于简单页面,MVVM 可能比 MVC 代码量更多
  3. ViewModel 可能膨胀:如果不注意,ViewModel 会变成新的 Massive ViewModel
  4. 数据绑定的隐式依赖:绑定链长时,调试困难

3.7 什么时候用 MVVM

适合 不适合
中型 App(10-50 个页面) 极简单的单页面 App
需要单元测试 不需要测试的项目
团队有 Combine/RxSwift 经验 团队完全没有响应式基础
需要数据绑定的复杂表单 静态页面为主
SwiftUI 项目 需要大量 UIKit 特定 API 的项目

四、VIPER:大型项目的选择

4.1 VIPER 的五个组件

VIPER 是 View / Interactor / Presenter / Entity / Router 的缩写,每个组件职责单一:

markdown 复制代码
┌─────────────┐
│    View     │ 负责 UI 渲染和用户输入捕获
└──────┬──────┘
       │
┌──────▼──────┐
│  Presenter  │ 格式化数据,准备显示,处理用户手势
└──────┬──────┘
       │
┌──────▼──────┐
│ Interactor  │ 业务逻辑和数据处理(无 UI 知识)
└──────┬──────┘
       │
┌──────▼──────┐
│   Entity    │ 纯数据模型
└─────────────┘
       │
┌──────▼──────┐
│   Router    │ 导航和模块间跳转
└─────────────┘

4.2 代码示例

swift 复制代码
// Entity - 纯数据
struct UserEntity {
    let id: UUID
    let name: String
    let email: String
}

// Interactor - 业务逻辑
protocol UserDetailInteractorProtocol {
    func fetchUser(id: UUID) async throws -> UserEntity
    func updateUser(_ user: UserEntity) async throws
}

class UserDetailInteractor: UserDetailInteractorProtocol {
    private let repository: UserRepository

    func fetchUser(id: UUID) async throws -> UserEntity {
        // 纯业务逻辑,没有任何 UI 代码
        try await repository.getUser(id: id)
    }

    func updateUser(_ user: UserEntity) async throws {
        try await repository.updateUser(user)
    }
}

// Presenter - 格式化显示
protocol UserDetailPresenterProtocol {
    func presentUser(_ user: UserEntity)
    func presentError(_ error: Error)
    func presentSaveSuccess()
}

class UserDetailPresenter: UserDetailPresenterProtocol {
    weak var view: UserDetailViewProtocol?
    private let interactor: UserDetailInteractorProtocol
    private let router: UserDetailRouterProtocol

    func presentUser(_ user: UserEntity) {
        let viewModel = UserViewModel(
            name: user.name,
            email: user.email,
            formattedDate: formatDate(user.createdAt)
        )
        view?.displayUser(viewModel)
    }

    private func formatDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        return formatter.string(from: date)
    }
}

// Router - 导航
protocol UserDetailRouterProtocol {
    func navigateToEditScreen(user: UserEntity)
    func dismiss()
}

class UserDetailRouter: UserDetailRouterProtocol {
    weak var viewController: UIViewController?

    func navigateToEditScreen(user: UserEntity) {
        let editVC = UserEditRouter.createModule(user: user)
        viewController?.navigationController?.pushViewController(editVC, animated: true)
    }
}

4.3 VIPER 的优点

  1. 职责极致单一:每个组件只做一件事
  2. 高度可测试:Interactor 可以独立于 UI 测试
  3. 适合大型团队:各组件可以并行开发
  4. 支持模块化:每个 VIPER 模块是完全独立的

4.4 VIPER 的缺点

  1. 代码量巨大:一个简单功能可能需要 5 个文件
  2. 学习成本高:新成员需要理解整个 VIPER 体系
  3. 过度设计风险:对于简单页面,VIPER 是杀鸡用牛刀
  4. 组件间通信复杂:需要定义大量协议

4.5 什么时候用 VIPER

适合 不适合
大型 App(50+ 个页面) 小中型 App
团队 10+ 人并行开发 小团队
需要模块间解耦 简单页面为主
长期维护的核心模块 快速迭代的 MVP
对测试覆盖率要求极高 测试不是优先项

五、Clean Architecture:企业级方案

5.1 Clean Architecture 分层

Clean Architecture(来自 Robert C. Martin)将系统分为多层,每层只依赖内层:

scss 复制代码
┌─────────────────────────────────────────────┐
│              Presentation Layer              │
│         (Views, ViewModels, Controllers)     │
└──────────────────────┬──────────────────────┘
                       │ 依赖
┌──────────────────────▼──────────────────────┐
│              Application Layer               │
│          (Use Cases, Commands)              │
└──────────────────────┬──────────────────────┘
                       │ 依赖
┌──────────────────────▼──────────────────────┐
│                Domain Layer                  │
│    (Entities, Repository Interfaces)         │
└──────────────────────▲──────────────────────┘
                       │ 实现
┌──────────────────────▼──────────────────────┐
│             Infrastructure Layer             │
│    (Repository Impl, API, DB, External)      │
└─────────────────────────────────────────────┘

5.2 代码示例

swift 复制代码
// ================= Domain Layer =================
// 实体 - 完全独立,不依赖任何外部框架
struct User: Equatable {
    let id: UUID
    let name: String
    let email: String
}

// 仓储接口 - 领域层定义接口,基础设施层实现
protocol UserRepository {
    func getUser(id: UUID) async throws -> User
    func saveUser(_ user: User) async throws
}

// ================= Application Layer =================
// Use Case - 应用业务逻辑
protocol GetUserUseCaseProtocol {
    func execute(userId: UUID) async throws -> User
}

class GetUserUseCase: GetUserUseCaseProtocol {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func execute(userId: UUID) async throws -> User {
        let user = try await repository.getUser(id: userId)
        // 可以在这里添加额外的业务规则
        return user
    }
}

// ================= Infrastructure Layer =================
// 仓储实现
class UserRepositoryImpl: UserRepository {
    private let networkClient: NetworkClient
    private let cache: UserCache

    func getUser(id: UUID) async throws -> User {
        // 先查缓存
        if let cached = cache.get(id) {
            return cached
        }
        // 再请求网络
        let dto = try await networkClient.get("/users/\(id.uuidString)")
        let user = UserDTO.toDomain(dto)
        cache.set(user)
        return user
    }

    func saveUser(_ user: User) async throws {
        try await networkClient.post("/users", body: UserDTO.fromDomain(user))
        cache.set(user)
    }
}

// ================= Presentation Layer =================
class UserDetailViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false

    private let getUserUseCase: GetUserUseCaseProtocol

    func loadUser(id: UUID) async {
        isLoading = true
        defer { isLoading = false }
        user = try? await getUserUseCase.execute(userId: id)
    }
}

5.3 Clean Architecture 的优点

  1. 依赖规则清晰:内层完全不知道外层存在
  2. 业务逻辑可独立测试:不依赖 UI、数据库、网络
  3. 技术替换成本低:换数据库、换网络库,只需改 Infrastructure 层
  4. 适合企业级项目:大型团队和长期维护

5.4 Clean Architecture 的缺点

  1. 初期投入大:需要定义大量接口和分层结构
  2. 代码量最多:最简单的功能也要跨越 4 层
  3. 需要 DI 容器:依赖注入的管理会很复杂
  4. 不适合快速迭代:前期设计时间较长

5.5 什么时候用 Clean Architecture

适合 不适合
企业级大型 App 小型 App
多团队协作 小团队
需要长期维护 短期项目
业务逻辑复杂且稳定 业务快速变化
需要可插拔的基础设施 技术栈固定

六、决策树:如何选架构

markdown 复制代码
项目启动
    │
    ▼
项目规模多大?
    │
    ├── < 10 个页面 ──→ MVC(直接用,别想太多)
    │
    ├── 10-50 个页面 ──→ 业务逻辑复杂吗?
    │       │
    │       ├── 否 ──→ MVVM(足够)
    │       │
    │       └── 是 ──→ MVVM + 模块化
    │               (按功能拆分模块,每个模块内部 MVVM)
    │
    └── 50+ 个页面 ──→ Clean Architecture
            │
            └── 团队 10+ 人? ──→ VIPER(模块内)
                              或按功能域拆分的 Clean Architecture

七、架构演进路径

7.1 渐进式演进

不要一开始就上最复杂的架构。推荐演进路径:

makefile 复制代码
阶段1: MVC(启动)
  └── 项目 < 10 个页面,或验证阶段

阶段2: MVC → MVVM(当 Controller 开始膨胀)
  └── 识别出数据绑定价值高的页面,先改造这些页面

阶段3: MVVM → Clean Architecture(当团队规模扩大)
  └── 按功能域拆分,每个域有自己的 Repository 和 Use Cases

阶段4: 模块化(当代码库太大)
  └── 抽出独立的 Module/Framework,独立仓库独立发布

7.2 迁移策略

永远不要大爆炸式重构。 正确的做法是:

  1. 新功能用新架构:新增功能直接用目标架构
  2. 渐进式迁移旧代码:当旧代码需要修改时,顺便迁移
  3. 保持新旧共存:在很长一段时间内,两种架构会同时存在
  4. 用测试保护:迁移前先加测试,确保迁移后行为不变

八、架构评审清单

无论选择哪种架构,上线前请用这个清单检查:

检查项 说明
Controller / ViewModel < 300 行 超过说明有膨胀趋势
View 只做 UI,不做逻辑 业务逻辑应该在 ViewModel / Interactor
Model 是纯数据 不包含网络、持久化等基础设施代码
依赖方向清晰 数据流向是否符合架构的预期方向
单元测试覆盖 ViewModel / Interactor 不能测试的代码说明架构有问题
模块边界明确 模块之间通过接口通信,不直接依赖

总结

架构 适用规模 代码量 测试友好度 学习曲线 推荐指数
MVC 小型 ⭐⭐⭐⭐
MVVM 中型 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
VIPER 大型 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
Clean Architecture 企业级 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐

记住:架构是工具,不是目标。选择能解决当前问题的最简单架构,然后在问题出现时再演进。


下篇预告

下一篇文章我们将深入分析 MVC 架构:为什么 Apple 默认选择它,以及什么时候应该「超越」它------识别 Massive View Controller 的早期信号,以及具体的拆分策略。


如果你觉得这篇文章有收获,欢迎点赞。你的支持是我持续输出的动力。

相关推荐
阿明in_AI1 小时前
"我让 AI 做的那个功能,到底有没有做?" —— 给 AI 加一个可视化 backlog
ai编程
析数塔1 小时前
AI项目失败的本质:认知失调而非技术瓶颈
人工智能·ai编程
冰凌时空2 小时前
Swift 类型系统入门:从 Int、String 到自定义类型
前端·ios·ai编程
hyunbar2 小时前
扣子(coze)高级实战-【今日头条】输入关键词批量采集,循环写入飞书多维表格
人工智能·ai编程
ftpeak2 小时前
LangGraph Agent 开发指南(10~子图 Subgraphs)
python·ai·langchain·ai编程·langgraph
AQ14_2 小时前
给AI编程上一道“紧箍咒”:Superpowers的军法式重构
重构·ai编程
极品小學生2 小时前
Claude Code 从零上手与进阶实战指南
ai编程
pop_xiaoli15 小时前
【iOS】autoreleasePool
ios·objective-c·cocoa
Java后端的Ai之路16 小时前
CodeBuddy-Rules配置
人工智能·python·ai编程