iOS 开发手册IOS DEVELOPMENT

CHAPTER 05代码组织 · ARCHITECTURE

应用架构

当界面与逻辑都在视图控制器里堆砌,代码就变成"Massive View Controller"。本章讲解如何用 MVC、MVVM、TCA、依赖注入与路由把代码分层、解耦、可测。

开始阅读 难度进阶 先修SwiftUI / UIKit 约2.5小时
05.1

为什么需要架构

架构的本质是"为了让人能改得动"。好的架构让代码可测试、可复用、可并行开发,并在需求变更时把改动范围关进最小边界。

没有银弹

架构是权衡,不是信仰。小项目用 MVC 完全够;状态复杂、多人协作时再引入 MVVM/TCA。先理解每种范式的"它解决什么问题",再决定要不要用。

05.2

MVC 的真相

苹果版 MVC 里 Controller 是 View 与 Model 的中介。它本不该臃肿——膨胀是因为开发者把本属 Model/Service 的逻辑塞进了 VC。

Model Controller View
MVC:Model 与 View 不直接通信,全部经由 Controller 中介。让 Controller 只做"胶水"是避免膨胀的关键。
05.3

MVVM 与绑定

MVVM 把状态与逻辑搬进 ViewModel,View(或 VC)只负责把 ViewModel 的数据"绑定"到界面。配合 SwiftUI 的 @Observable 或 Combine,绑定几乎是自动的。

// ViewModel:不依赖任何 UI 框架,纯 Swift 可测
@Observable
final class LoginViewModel {
    var username = ""
    var password = ""
    private(set) var isLoading = false

    func login() async {
        isLoading = true
        defer { isLoading = false }
        // 调用 service...
    }
}

// View:只描述"状态 -> 界面"
struct LoginView: View {
    @State private var vm = LoginViewModel()
    var body: some View {
        TextField("用户名", text: $vm.username)
        SecureField("密码", text: $vm.password)
        Button("登录") { Task { await vm.login() } }
            .disabled(vm.isLoading)
    }
}
ViewModel 不持有 UI

保持 ViewModel 与 UIKit/SwiftUI 解耦,它就能脱离界面做单元测试:直接调用方法、断言状态字段,无需启动 App。

05.4

TCA 单向数据流

The Composable Architecture 把状态变化收束成"状态 + 动作 + Reducer"的纯函数:所有变更经一个入口,便于测试、回放与推理。

@Reducer
struct Counter {
    struct State { var count = 0 }
    enum Action { case increment, decrement }
    var body: some ReducerOf {
        Reduce { state, action in
            switch action {
            case .increment: state.count += 1; return .none
            case .decrement: state.count -= 1; return .none
            }
        }
    }
}
何时上 TCA

TCA 适合状态交互复杂、需要强可测性的中大型项目。它带来学习成本与样板代码,小项目用 MVVM 更划算。先懂单向数据流的思想,再决定是否引入框架。

05.5

依赖注入

不要在被依赖方里直接 new 具体实现,而是把"接口"传进去。这样生产用真实网络、测试用 Mock,互不干扰。

protocol UserRepository {
    func currentUser() async throws -> User
}

struct APIUserRepository: UserRepository { /* 真实实现 */ }

final class ProfileViewModel {
    private let repo: UserRepository
    init(repo: UserRepository) { self.repo = repo }   // 注入,而非内部创建
}

// 测试时传入 Mock
let vm = ProfileViewModel(repo: MockUserRepository())
05.6

Coordinator 路由

把"页面跳转逻辑"从 ViewController 里抽出来交给 Coordinator,VC 只发"我想去某处"的事件,由 Coordinator 决定如何导航。这样 VC 可独立测试、复用。

protocol LoginCoordinator {
    func showForgotPassword()
    func didLogin()
}
// ViewController 持有 coordinator 协议,不直接 present 下一个 VC
05.7

模块拆分与分层

用 Swift Package 或本地 module 把代码按"特性/层"切分,控制编译耦合与可见性。典型分层:UI 层 → 业务/状态层 → 服务层 → 基础设施层。

层职责依赖方向
UI 层SwiftUI/UIKit 视图与交互依赖下层
业务层ViewModel / Use Case / Reducer依赖服务接口
服务层网络、持久化、定位等实现依赖基础设施
基础设施底层库、工具、第三方封装不依赖上层
依赖方向只能向下

上层依赖下层,下层绝不反向引用上层,否则形成循环依赖、无法单独测试。接口(协议)是跨层解耦的关键武器。

05.8

常见陷阱与 FAQ

ViewModel 里能 import UIKit 吗?

尽量避免。一旦引入 UIKit/SwiftUI,它就绑死在 UI 框架上,无法脱离界面做单元测试。颜色、字体等"展示细节"应在 View 层决定,ViewModel 只持有数据。

MVVM 的绑定在 UIKit 里怎么做?

用 Combine 或 RxSwift 把 ViewModel 的发布者订阅到 UI 控件;iOS 17+ 也可让 ViewModel 遵循 @Observable,在 VC 里 KVO 式观察。SwiftUI 则靠属性包装器自动绑定,最省心。

什么时候不该用 TCA?

原型、单人小项目、状态极其简单的页面。TCA 的样板与概念成本在小规模下是负收益。它的价值随状态复杂度与团队规模上升而凸显。

05.9

实战练习与资源

延伸资源