专栏 :iOS高级工程实践
编号 :C01 · 系列第 1 篇
字数 :约 6000 字
标签:iOS / 架构设计 / MVC / MVVM / VIPER / Clean Architecture / 架构选型
前言
「这个 App 架构选什么好?」
这是每个 iOS 工程师在项目初期都会面临的问题。网上有无数文章讲 MVC、MVVM、VIPER、Clean Architecture,但大多数只讲概念,不讲取舍。
今天,我们换一种方式:不只讲「是什么」,更讲「什么时候适合」,以及「换架构的代价是什么」。
读完这篇文章,你会得到:
- 决策树:根据项目特征快速匹配适合的架构
- 各架构的权衡分析:不只是优点,还有你必须付出的代价
- 渐进式迁移路径:如何从最简单的方式逐步演进到更复杂的架构
一、架构的本质是什么
在深入细节之前,我们需要先回答一个问题:架构到底是什么?
很多人把架构等同于「目录结构」或者「分层方式」,但这只是表面。
架构的本质是:系统中各组件之间 **关系**的定义。
这些关系包括:
- 数据流向:数据从哪里来,经过哪些处理,流向哪里
- 依赖方向:谁依赖谁,谁不应该知道谁的存在
- 边界划分:哪些代码属于同一模块,模块之间如何通信
- 变更隔离:当需求变化时,需要改多少代码,改哪些文件
好的架构让变更容易 ,坏的架构让变更痛苦。
二、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 的优点
- 简单直接:概念少,新人容易理解
- Apple 原生支持:Xcode 的模板默认就是 MVC
- 适合小型项目:对于功能简单的 App,MVC 的效率最高
- 代码量少:不需要额外的抽象层
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 的优点
- 测试友好:ViewModel 不持有任何 UI 组件,可以直接测试
- Controller 瘦身:ViewController 从 500 行变成 50 行
- 数据绑定减少样板代码:不需要手动同步 UI 和数据
- 团队协作友好:View 和 ViewModel 可以并行开发
3.6 MVVM 的缺点
- 学习曲线:需要理解响应式编程或 Combine
- 过度工程风险:对于简单页面,MVVM 可能比 MVC 代码量更多
- ViewModel 可能膨胀:如果不注意,ViewModel 会变成新的 Massive ViewModel
- 数据绑定的隐式依赖:绑定链长时,调试困难
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 的优点
- 职责极致单一:每个组件只做一件事
- 高度可测试:Interactor 可以独立于 UI 测试
- 适合大型团队:各组件可以并行开发
- 支持模块化:每个 VIPER 模块是完全独立的
4.4 VIPER 的缺点
- 代码量巨大:一个简单功能可能需要 5 个文件
- 学习成本高:新成员需要理解整个 VIPER 体系
- 过度设计风险:对于简单页面,VIPER 是杀鸡用牛刀
- 组件间通信复杂:需要定义大量协议
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 的优点
- 依赖规则清晰:内层完全不知道外层存在
- 业务逻辑可独立测试:不依赖 UI、数据库、网络
- 技术替换成本低:换数据库、换网络库,只需改 Infrastructure 层
- 适合企业级项目:大型团队和长期维护
5.4 Clean Architecture 的缺点
- 初期投入大:需要定义大量接口和分层结构
- 代码量最多:最简单的功能也要跨越 4 层
- 需要 DI 容器:依赖注入的管理会很复杂
- 不适合快速迭代:前期设计时间较长
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 迁移策略
永远不要大爆炸式重构。 正确的做法是:
- 新功能用新架构:新增功能直接用目标架构
- 渐进式迁移旧代码:当旧代码需要修改时,顺便迁移
- 保持新旧共存:在很长一段时间内,两种架构会同时存在
- 用测试保护:迁移前先加测试,确保迁移后行为不变
八、架构评审清单
无论选择哪种架构,上线前请用这个清单检查:
| 检查项 | 说明 |
|---|---|
| Controller / ViewModel < 300 行 | 超过说明有膨胀趋势 |
| View 只做 UI,不做逻辑 | 业务逻辑应该在 ViewModel / Interactor |
| Model 是纯数据 | 不包含网络、持久化等基础设施代码 |
| 依赖方向清晰 | 数据流向是否符合架构的预期方向 |
| 单元测试覆盖 ViewModel / Interactor | 不能测试的代码说明架构有问题 |
| 模块边界明确 | 模块之间通过接口通信,不直接依赖 |
总结
| 架构 | 适用规模 | 代码量 | 测试友好度 | 学习曲线 | 推荐指数 |
|---|---|---|---|---|---|
| MVC | 小型 | ⭐ | ⭐ | ⭐ | ⭐⭐⭐⭐ |
| MVVM | 中型 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| VIPER | 大型 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| Clean Architecture | 企业级 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
记住:架构是工具,不是目标。选择能解决当前问题的最简单架构,然后在问题出现时再演进。
下篇预告
下一篇文章我们将深入分析 MVC 架构:为什么 Apple 默认选择它,以及什么时候应该「超越」它------识别 Massive View Controller 的早期信号,以及具体的拆分策略。
如果你觉得这篇文章有收获,欢迎点赞。你的支持是我持续输出的动力。