Model-View-Controller
1. Model层:唯一真理来源
职责:封装数据、业务逻辑、持久化,绝对不依赖UIKit,不持有Controller/View。 代码示例:
import Foundation
// 录音数据模型
struct Recording: Codable, Equatable {
let uuid: UUID
let name: String
let duration: TimeInterval
let createDate: Date
var fileURL: URL {
Store.documentsDirectory.appendingPathComponent(uuid.uuidString).appendingPathExtension("m4a")
}
}
// 文件夹模型(业务逻辑封装)
class Folder: Codable, Equatable {
let uuid: UUID
var name: String
private(set) var contents: [AnyItem] // 禁止外部直接修改
init(uuid: UUID = UUID(), name: String, contents: [AnyItem] = []) {
self.uuid = uuid
self.name = name
self.contents = contents
}
static func == (lhs: Folder, rhs: Folder) -> Bool { lhs.uuid == rhs.uuid }
// 业务逻辑封装在Model内
func addRecording(_ recording: Recording) {
contents.append(AnyItem(recording))
Store.shared.save()
}
func removeItem(at index: Int) {
guard index < contents.count else { return }
contents.remove(at: index)
Store.shared.save()
}
}
// 全局持久化Store
final class Store {
static let shared = Store()
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let changedNotification = Notification.Name("StoreStateChanged")
private let storeFileURL: URL
private(set) var rootFolder: Folder
private init() {
self.storeFileURL = Store.documentsDirectory.appendingPathComponent("VoiceStore.json")
do {
let data = try Data(contentsOf: storeFileURL)
self.rootFolder = try JSONDecoder().decode(Folder.self, from: data)
} catch {
self.rootFolder = Folder(name: "我的录音")
}
}
func save() {
do {
let data = try JSONEncoder().encode(rootFolder)
try data.write(to: storeFileURL, options: .atomic)
NotificationCenter.default.post(name: Store.changedNotification, object: nil)
} catch {
print("持久化失败: \(error)")
}
}
}
2. View层:纯界面展示与事件转发
职责:负责UI渲染、捕获用户事件并转发,不持有Model,无业务逻辑。 代码示例:
import UIKit
protocol RecordViewDelegate: AnyObject {
func recordViewDidStartRecording(_ view: RecordView)
func recordViewDidStopRecording(_ view: RecordView)
}
class RecordView: UIView {
weak var delegate: RecordViewDelegate?
var recordTime: TimeInterval = 0 {
didSet { timeLabel.text = String(format: "%.1f秒", recordTime) }
}
var isRecording: Bool = false {
didSet { recordButton.isSelected = isRecording }
}
private let recordButton = UIButton(type: .system)
private let timeLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupActions()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupSubviews() {
backgroundColor = .white
recordButton.setTitle("开始录音", for: .normal)
recordButton.setTitle("停止录音", for: .selected)
addSubview(recordButton)
addSubview(timeLabel)
// 布局代码省略
}
private func setupActions() {
recordButton.addTarget(self, action: #selector(recordButtonTapped), for: .touchUpInside)
}
@objc private func recordButtonTapped() {
isRecording ? delegate?.recordViewDidStopRecording(self) : delegate?.recordViewDidStartRecording(self)
}
}
3. Controller层:唯一中介者
职责:管理View生命周期、监听Model变更、接收View事件、协调页面跳转,不实现业务/UI细节。
二、Cocoa MVC标准通信机制
| 通信方向 | 合法方式 | 禁止行为 |
|---|---|---|
| Controller→Model | 直接持有,调用公开方法 | 直接修改私有属性 |
| Controller→View | 直接持有,设置公开属性 | 直接修改内部子View |
| View→Controller | Target-Action、Delegate | 直接持有/修改Model |
| Model→Controller | Notification、KVO | 直接持有Controller |
三、常见误区(每个误区配错误+正确代码)
误区1:业务逻辑写在Controller中
错误示例:
// 错误:业务逻辑散落在Controller
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
folder.contents.remove(at: indexPath.row) // 直接修改Model数组
try? Data(contentsOf: storeFileURL).write(to: storeFileURL) // 直接持久化
tableView.reloadData()
}
}
正确示例:
// 正确:业务逻辑封装在Model内
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
folder.removeItem(at: indexPath.row) // 仅调用Model方法
}
}
误区2:UI逻辑写在Controller中
错误示例:
// 错误:UI细节写在Controller
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let item = folder.contents[indexPath.row]
cell.textLabel?.text = item.name
cell.textLabel?.textColor = .systemBlue // 直接修改样式
cell.detailTextLabel?.text = item.subtitle
return cell
}
正确示例:
// 正确:自定义Cell封装UI逻辑
class ItemCell: UITableViewCell {
func configure(with item: AnyItem) {
textLabel?.text = item.name
textLabel?.textColor = .systemBlue // 样式封装在Cell内
detailTextLabel?.text = item.subtitle
}
}
// Controller仅传数据
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
cell.configure(with: folder.contents[indexPath.row])
return cell
}
误区3:DataSource/Delegate耦合在Controller中
错误示例:
// 错误:所有TableView逻辑耦合在Controller
class FolderViewController: UITableViewController {
override func numberOfSections(in tableView: UITableView) -> Int { 1 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { folder.contents.count }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { /* 省略 */ }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { /* 省略 */ }
}
正确示例:见「优化方案3」。
误区4:Model与Controller双向依赖
错误示例:
// 错误:Model直接持有Controller
class Folder {
weak var viewController: FolderViewController? // 双向依赖
func addRecording(_ recording: Recording) {
contents.append(AnyItem(recording))
viewController?.tableView.reloadData() // 直接调用Controller
}
}
正确示例:Model仅通过Notification广播变更,见「Model层代码」。
误区5:View直接修改Model
错误示例:
// 错误:View直接持有并修改Model
class RecordView: UIView {
var folder: Folder? // View持有Model
@objc private func stopButtonTapped() {
let recording = Recording(/* 省略 */)
folder?.addRecording(recording) // 直接修改Model,跳过Controller
}
}
正确示例:View仅通过Delegate转发事件,见「View层代码」。
四、优化方案(每个方案配代码)
优化1:业务逻辑100%下沉到Model
代码示例:同「误区1正确示例」,核心是将数据修改、持久化逻辑封装在Model的公开方法中。
优化2:UI逻辑100%封装到自定义View/Cell
代码示例:同「误区2正确示例」,核心是自定义View/Cell,暴露配置方法,隐藏内部实现。
优化3:抽离DataSource/Delegate独立对象
代码示例:
// 抽离的独立DataSource
class FolderDataSource: NSObject, UITableViewDataSource {
var folder: Folder
init(folder: Folder) {
self.folder = folder
super.init()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
folder.contents.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
cell.configure(with: folder.contents[indexPath.row])
return cell
}
}
// 抽离的独立Delegate
class FolderDelegate: NSObject, UITableViewDelegate {
weak var viewController: FolderViewController?
var folder: Folder
init(viewController: FolderViewController, folder: Folder) {
self.viewController = viewController
self.folder = folder
super.init()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let item = folder.contents[indexPath.row].recording else { return }
let playVC = PlayViewController(recording: item)
viewController?.navigationController?.pushViewController(playVC, animated: true)
}
}
// 优化后的Controller
class FolderViewController: UITableViewController {
var folder: Folder!
private var dataSource: FolderDataSource!
private var delegate: FolderDelegate!
override func viewDidLoad() {
super.viewDidLoad()
dataSource = FolderDataSource(folder: folder)
delegate = FolderDelegate(viewController: self, folder: folder)
tableView.dataSource = dataSource
tableView.delegate = delegate
tableView.register(ItemCell.self, forCellReuseIdentifier: "ItemCell")
}
}
优化4:统一观察者模式处理Model变更
代码示例:
// Store扩展,统一观察者封装
extension Store {
static func addObserver(
_ observer: AnyObject,
handler: @escaping (Folder) -> Void
) -> NSObjectProtocol {
handler(Store.shared.rootFolder) // 立即同步初始状态
return NotificationCenter.default.addObserver(
forName: Store.changedNotification,
object: nil,
queue: .main
) { _ in
handler(Store.shared.rootFolder)
}
}
}
// Controller中使用
class FolderViewController: UITableViewController {
private var observer: Any?
override func viewDidLoad() {
super.viewDidLoad()
observer = Store.addObserver(self) { [weak self] rootFolder in
guard let self = self else { return }
self.folder = rootFolder
self.dataSource.folder = rootFolder
self.delegate.folder = rootFolder
self.tableView.reloadData()
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
优化5:依赖注入降低耦合
代码示例:
// 依赖注入的Controller初始化
class FolderViewController: UITableViewController {
private let folder: Folder
private let store: Store
// 初始化时注入依赖,不硬编码单例
init(folder: Folder, store: Store = .shared) {
self.folder = folder
self.store = store
super.init(style: .plain)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
// 业务使用时注入
let vc = FolderViewController(folder: Store.shared.rootFolder)
navigationController?.pushViewController(vc, animated: true)
// 测试时注入Mock对象
class MockStore: Store {
override func save() { /* 测试用空实现 */ }
}
let mockFolder = Folder(name: "测试文件夹")
let mockStore = MockStore()
let testVC = FolderViewController(folder: mockFolder, store: mockStore)
五、原书结论
绝大多数MVC问题源于未遵守职责边界,严格按本章规范实现的MVC,完全可满足中大型项目需求,具备高可测试性、低维护成本的优势。
Model-View-ViewModel+协调器(MVVM-C)
1. Model层:与MVC一致,唯一真理来源
职责:封装数据、业务逻辑、持久化,不依赖UIKit,不持有View/ViewModel。 代码示例(与第三章核心一致,简化展示):
import Foundation
struct Recording: Codable, Equatable {
let uuid: UUID
let name: String
let duration: TimeInterval
let createDate: Date
var fileURL: URL {
Store.documentsDirectory.appendingPathComponent(uuid.uuidString).appendingPathExtension("m4a")
}
}
class Folder: Codable, Equatable {
let uuid: UUID
var name: String
private(set) var contents: [AnyItem]
// 业务逻辑封装
func addRecording(_ recording: Recording) { /* 同第三章 */ }
func removeItem(at index: Int) { /* 同第三章 */ }
}
final class Store {
static let shared = Store()
static let changedNotification = Notification.Name("StoreStateChanged")
private(set) var rootFolder: Folder
// 持久化逻辑同第三章
}
2. View层:View + ViewController(统一属于View层)
职责:负责UI渲染、用户交互捕获,持有ViewModel,但不持有Model,仅通过ViewModel获取展示数据。 核心原则:ViewController仅做UI绑定与事件转发,不实现业务逻辑。
3. ViewModel层:业务逻辑与数据转换核心
职责:
- 持有Model,处理业务逻辑(如数据加载、删除、筛选)
- 将Model数据转换为View可直接展示的格式(如时间格式化、状态映射)
- 不持有View,通过数据绑定机制向View通知状态变更 代码示例(基础版ViewModel):
import Foundation
class RecordListViewModel {
// 持有Model
private let folder: Folder
private let store: Store
// 暴露给View的展示数据(原始数据转换后)
var recordCount: Int { folder.contents.count }
var folderName: String { folder.name }
// 数据绑定回调:View层设置此闭包,监听数据变更
var onDataChanged: (() -> Void)?
var onError: ((String) -> Void)?
init(folder: Folder, store: Store = .shared) {
self.folder = folder
self.store = store
// 监听Model变更
NotificationCenter.default.addObserver(
self,
selector: #selector(modelStateChanged),
name: Store.changedNotification,
object: nil
)
}
// 数据转换方法:将Model转换为View直接可用的格式
func recordDisplayInfo(at index: Int) -> (name: String, subtitle: String) {
let item = folder.contents[index]
let name = item.name
let duration = String(format: "%.1f秒", item.recording?.duration ?? 0)
let date = DateFormatter.localizedString(from: item.recording?.createDate ?? Date(), dateStyle: .short, timeStyle: .short)
return (name, "\(duration) · \(date)")
}
// 业务逻辑方法:View仅调用,不实现
func deleteRecord(at index: Int) {
guard index < folder.contents.count else {
onError?("删除失败:索引越界")
return
}
folder.removeItem(at: index)
}
@objc private func modelStateChanged() {
// 通知View层更新
onDataChanged?()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
二、MVVM标准通信机制
| 通信方向 | 合法方式 | 禁止行为 |
|---|---|---|
| View→ViewModel | 直接调用ViewModel公开方法、设置数据绑定闭包 | 直接持有/修改Model |
| ViewModel→Model | 直接持有,调用Model公开方法 | 直接修改Model私有属性 |
| Model→ViewModel | Notification、KVO、闭包回调 | 直接持有ViewModel |
| ViewModel→View | 数据绑定(闭包、响应式框架) | 直接持有View对象 |
三、常见误区(每个误区配错误+正确代码)
误区1:ViewModel直接持有View(导致循环引用)
错误示例:
// 错误:ViewModel直接持有ViewController
class RecordListViewModel {
weak var viewController: RecordListViewController? // 持有View层对象
func deleteRecord(at index: Int) {
folder.removeItem(at: index)
viewController?.tableView.reloadData() // 直接调用View方法
}
}
class RecordListViewController: UITableViewController {
var viewModel: RecordListViewModel! // 持有ViewModel
override func viewDidLoad() {
super.viewDidLoad()
viewModel.viewController = self // 双向持有,易循环引用
}
}
正确示例:
// 正确:通过闭包绑定,不持有View
class RecordListViewModel {
// 仅暴露回调闭包,不持有View
var onDataChanged: (() -> Void)?
func deleteRecord(at index: Int) {
folder.removeItem(at: index)
onDataChanged?() // 通过闭包通知View
}
}
class RecordListViewController: UITableViewController {
var viewModel: RecordListViewModel!
override func viewDidLoad() {
super.viewDidLoad()
// 设置数据绑定闭包
viewModel.onDataChanged = { [weak self] in
guard let self = self else { return }
self.tableView.reloadData()
}
}
}
误区2:把UI逻辑放在ViewModel中
错误示例:
// 错误:ViewModel包含UI逻辑
class RecordListViewModel {
// 直接返回UIColor,依赖UIKit,违反职责
func recordTextColor(at index: Int) -> UIColor {
let item = folder.contents[index]
return item.name.isEmpty ? .red : .black
}
// 直接返回UIFont,依赖UIKit
func recordFont() -> UIFont {
.systemFont(ofSize: 16)
}
}
正确示例:
// 正确:ViewModel仅返回业务状态,UI逻辑在View层处理
class RecordListViewModel {
// 返回业务状态,不依赖UIKit
func isRecordNameEmpty(at index: Int) -> Bool {
folder.contents[index].name.isEmpty
}
}
class RecordListViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
let displayInfo = viewModel.recordDisplayInfo(at: indexPath.row)
cell.nameLabel.text = displayInfo.name
cell.subtitleLabel.text = displayInfo.subtitle
// UI逻辑在View层处理
cell.nameLabel.textColor = viewModel.isRecordNameEmpty(at: indexPath.row) ? .red : .black
cell.nameLabel.font = .systemFont(ofSize: 16)
return cell
}
}
误区3:数据绑定过度复杂,引入不必要的第三方库
错误示例:
// 错误:简单场景过度使用RxSwift,增加学习成本
import RxSwift
import RxCocoa
class RecordListViewModel {
let records = BehaviorRelay<[AnyItem]>(value: [])
private let disposeBag = DisposeBag()
init(folder: Folder) {
// 过度复杂的绑定链
NotificationCenter.default.rx.notification(Store.changedNotification)
.map { _ in folder.contents }
.bind(to: records)
.disposed(by: disposeBag)
}
}
正确示例:见「优化方案1」,轻量级闭包绑定即可满足需求。
误区4:Model和ViewModel职责不清,业务逻辑分散
错误示例:
// 错误:业务逻辑分散在ViewModel和Model中
class RecordListViewModel {
func deleteRecord(at index: Int) {
guard index < folder.contents.count else { return }
// ViewModel直接操作Model数组,业务逻辑越权
folder.contents.remove(at: index)
// ViewModel直接处理持久化,职责混乱
try? JSONEncoder().encode(folder).write(to: storeFileURL)
NotificationCenter.default.post(name: Store.changedNotification, object: nil)
}
}
正确示例:
// 正确:业务逻辑完全封装在Model中
class Folder {
// Model内部实现删除与持久化
func removeItem(at index: Int) {
guard index < contents.count else { return }
contents.remove(at: index)
Store.shared.save()
}
}
class RecordListViewModel {
func deleteRecord(at index: Int) {
// ViewModel仅调用Model方法
folder.removeItem(at: index)
}
}
四、优化方案(每个方案配代码)
优化1:轻量级闭包实现数据绑定(无需第三方库)
核心思路:用简单闭包替代复杂响应式框架,满足80%场景需求。 代码示例:
// ViewModel:定义绑定闭包
class RecordListViewModel {
// 数据变更回调
var onDataChanged: (() -> Void)?
// 错误回调
var onError: ((String) -> Void)?
// 加载状态回调
var onLoadingStateChanged: ((Bool) -> Void)?
func loadRecords() {
onLoadingStateChanged?(true)
// 模拟异步加载
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
self.onLoadingStateChanged?(false)
self.onDataChanged?()
}
}
}
// ViewController:设置绑定闭包
class RecordListViewController: UITableViewController {
var viewModel: RecordListViewModel!
private let loadingIndicator = UIActivityIndicatorView(style: .large)
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
viewModel.loadRecords()
}
private func setupBindings() {
// 绑定数据变更
viewModel.onDataChanged = { [weak self] in
guard let self = self else { return }
self.tableView.reloadData()
}
// 绑定错误
viewModel.onError = { [weak self] message in
guard let self = self else { return }
let alert = UIAlertController(title: "错误", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self.present(alert, animated: true)
}
// 绑定加载状态
viewModel.onLoadingStateChanged = { [weak self] isLoading in
guard let self = self else { return }
isLoading ? self.loadingIndicator.startAnimating() : self.loadingIndicator.stopAnimating()
}
}
}
优化2:引入Combine实现响应式绑定(iOS 13+)
核心思路:对于需要复杂数据流的场景,使用系统原生Combine框架,无需第三方依赖。 代码示例:
import Combine
class RecordListViewModel {
// 用CurrentValueSubject暴露状态
private(set) var records = CurrentValueSubject<[AnyItem], Never>([])
private(set) var isLoading = CurrentValueSubject<Bool, Never>(false)
private(set) var error = PassthroughSubject<String, Never>()
private let folder: Folder
private var cancellables = Set<AnyCancellable>()
init(folder: Folder) {
self.folder = folder
// 绑定Model变更
NotificationCenter.default.publisher(for: Store.changedNotification)
.map { _ in folder.contents }
.assign(to: \.value, on: records)
.store(in: &cancellables)
}
func loadRecords() {
isLoading.send(true)
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
self.isLoading.send(false)
self.records.send(folder.contents)
}
}
func deleteRecord(at index: Int) {
guard index < records.value.count else {
error.send("索引越界")
return
}
folder.removeItem(at: index)
}
}
// ViewController中绑定
class RecordListViewController: UITableViewController {
var viewModel: RecordListViewModel!
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
viewModel.loadRecords()
}
private func setupBindings() {
// 绑定列表数据
viewModel.records
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.tableView.reloadData()
}
.store(in: &cancellables)
// 绑定加载状态
viewModel.isLoading
.receive(on: DispatchQueue.main)
.sink { [weak self] isLoading in
isLoading ? self?.loadingIndicator.startAnimating() : self?.loadingIndicator.stopAnimating()
}
.store(in: &cancellables)
}
}
优化3:ViewModel依赖注入,提升可测试性
核心思路:通过初始化注入Model依赖,便于测试时注入Mock对象。 代码示例:
// ViewModel支持依赖注入
class RecordListViewModel {
private let folder: Folder
private let store: Store
// 初始化时注入,默认使用单例
init(folder: Folder, store: Store = .shared) {
self.folder = folder
self.store = store
}
}
// 测试时注入Mock对象
class MockFolder: Folder {
var deleteRecordCalled = false
override func removeItem(at index: Int) {
deleteRecordCalled = true
// 测试用空实现
}
}
// 测试代码
func testDeleteRecord() {
let mockFolder = MockFolder(name: "测试文件夹")
let viewModel = RecordListViewModel(folder: mockFolder)
viewModel.deleteRecord(at: 0)
XCTAssertTrue(mockFolder.deleteRecordCalled, "删除方法应被调用")
}
优化4:拆分复杂ViewModel,按功能模块化
核心思路:当ViewModel过于复杂时,按功能拆分为多个子ViewModel(如列表ViewModel+详情ViewModel)。 代码示例:
// 拆分后的列表项ViewModel
class RecordItemViewModel {
private let recording: Recording
var name: String { recording.name }
var durationText: String { String(format: "%.1f秒", recording.duration) }
var dateText: String {
DateFormatter.localizedString(from: recording.createDate, dateStyle: .short, timeStyle: .short)
}
init(recording: Recording) {
self.recording = recording
}
}
// 主ViewModel使用子ViewModel
class RecordListViewModel {
private let folder: Folder
// 直接返回子ViewModel数组,简化数据转换
var itemViewModels: [RecordItemViewModel] {
folder.contents.compactMap { $0.recording }.map { RecordItemViewModel(recording: $0) }
}
init(folder: Folder) {
self.folder = folder
}
}
// ViewController中使用子ViewModel
class RecordListViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
let itemVM = viewModel.itemViewModels[indexPath.row]
cell.nameLabel.text = itemVM.name
cell.subtitleLabel.text = "\(itemVM.durationText) · \(itemVM.dateText)"
return cell
}
}
五、原书结论
MVVM通过引入ViewModel分离业务逻辑与数据转换,配合数据绑定机制,可显著瘦身ViewController,提升代码可测试性。对于简单场景,使用轻量级闭包绑定即可;对于复杂数据流,可引入Combine等响应式框架。核心是严格遵守职责边界,避免ViewModel持有View或包含UI逻辑。
《iOS 编程之道》第四章(71-97页)MVVM架构模式 知识点完整总结(RxSwift实现版)
本章核心是引入MVVM(Model-View-ViewModel) 架构,通过RxSwift实现响应式数据绑定,解决MVC中Controller臃肿问题,提升代码可测试性与数据流清晰度。所有示例基于前章录音App场景,结合RxSwift 6.0+语法实现。
非常抱歉,之前的回复遗漏了本章的核心扩展模块——协调器(Coordinator),原书第四章71-97页的完整架构是 MVVM+C(Model-View-ViewModel + Coordinator),核心是在标准MVVM的基础上,通过协调器剥离ViewController的页面导航与流程管理职责,彻底解决ViewController耦合、职责臃肿、无法复用的问题。
以下是完整的第四章知识点总结,包含MVVM核心、协调器全量内容,全程配套RxSwift代码、误区正反示例与优化方案。
《iOS 编程之道》第四章(71-97页)MVVM+C 架构模式 知识点完整总结(RxSwift实现版)
本章核心是在MVVM架构基础上引入协调器(Coordinator),形成完整的MVVM+C四层架构,通过RxSwift实现全链路响应式绑定,彻底解决MVC/MVVM中ViewController职责臃肿、页面强耦合、无法复用的问题。所有示例基于录音App场景,适配RxSwift 6.0+语法。
一、MVVM+C 完整架构:四层角色的严格职责划分
1. 核心架构层级与职责
| 层级 | 核心职责 | 强制约束 |
|---|---|---|
| Model | 数据封装、业务逻辑、持久化 | 绝对不依赖UIKit/RxSwift,不持有其他层级对象 |
| ViewModel | 数据格式转换、状态管理、业务逻辑转发 | 不持有View/Coordinator,不包含UI逻辑 |
| View(ViewController) | UI渲染、用户事件捕获、ViewModel绑定 | 不直接跳转页面、不持有其他VC、不实现业务逻辑 |
| Coordinator(协调器) | 导航管理、VC/ViewModel创建、依赖注入、业务流程编排 | 不包含UI逻辑、不实现业务逻辑、仅持有导航控制器 |
2. 核心基础代码
(1)Model层(与原书一致,无修改)
import Foundation
struct Recording: Codable, Equatable {
let uuid: UUID
let name: String
let duration: TimeInterval
let createDate: Date
var fileURL: URL {
Store.documentsDirectory.appendingPathComponent(uuid.uuidString).appendingPathExtension("m4a")
}
}
class Folder: Codable, Equatable {
let uuid: UUID
var name: String
private(set) var contents: [AnyItem]
init(uuid: UUID = UUID(), name: String, contents: [AnyItem] = []) {
self.uuid = uuid
self.name = name
self.contents = contents
}
static func == (lhs: Folder, rhs: Folder) -> Bool { lhs.uuid == rhs.uuid }
func addRecording(_ recording: Recording) {
contents.append(AnyItem(recording))
Store.shared.save()
}
func removeItem(at index: Int) {
guard index < contents.count else { return }
contents.remove(at: index)
Store.shared.save()
}
}
final class Store {
static let shared = Store()
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let changedNotification = Notification.Name("StoreStateChanged")
private let storeFileURL: URL
private(set) var rootFolder: Folder
private init() {
self.storeFileURL = Store.documentsDirectory.appendingPathComponent("VoiceStore.json")
do {
let data = try Data(contentsOf: storeFileURL)
self.rootFolder = try JSONDecoder().decode(Folder.self, from: data)
} catch {
self.rootFolder = Folder(name: "我的录音")
}
}
func save() {
do {
let data = try JSONEncoder().encode(rootFolder)
try data.write(to: storeFileURL, options: .atomic)
NotificationCenter.default.post(name: Store.changedNotification, object: nil)
} catch {
print("持久化失败: \(error)")
}
}
}
(2)ViewModel层(RxSwift Input/Output 标准模式)
import Foundation
import RxSwift
import RxCocoa
class RecordListViewModel {
// 输入:View层的用户事件
struct Input {
let deleteTrigger: Observable<Int>
let loadTrigger: Observable<Void>
}
// 输出:ViewModel暴露给View的状态
struct Output {
let records: Driver<[AnyItem]>
let isLoading: Driver<Bool>
let error: Driver<String>
}
private let folder: Folder
private let store: Store
private let disposeBag = DisposeBag()
// 依赖注入,不硬编码单例
init(folder: Folder, store: Store = .shared) {
self.folder = folder
self.store = store
}
// 核心:绑定输入事件,生成输出状态
func transform(input: Input) -> Output {
let isLoading = BehaviorRelay<Bool>(value: false)
let error = PublishRelay<String>()
// 监听Model变更,同步列表数据
let records = NotificationCenter.default.rx.notification(Store.changedNotification)
.map { [weak self] _ in self?.folder.contents ?? [] }
.startWith(folder.contents)
.asDriver(onErrorJustReturn: [])
// 处理加载事件
input.loadTrigger
.do(onNext: { isLoading.accept(true) })
.delay(.milliseconds(500), scheduler: MainScheduler.instance)
.do(onNext: { isLoading.accept(false) })
.subscribe()
.disposed(by: disposeBag)
// 处理删除事件
input.deleteTrigger
.subscribe(onNext: { [weak self] index in
guard let self = self else { return }
guard index < self.folder.contents.count else {
error.accept("删除失败:索引越界")
return
}
self.folder.removeItem(at: index)
})
.disposed(by: disposeBag)
return Output(
records: records,
isLoading: isLoading.asDriver(),
error: error.asDriver(onErrorJustReturn: "")
)
}
// 数据转换:Model → View可直接展示的格式
func recordDisplayInfo(for item: AnyItem) -> (name: String, subtitle: String) {
let name = item.name
let duration = String(format: "%.1f秒", item.recording?.duration ?? 0)
let date = DateFormatter.localizedString(from: item.recording?.createDate ?? Date(), dateStyle: .short, timeStyle: .short)
return (name, "\(duration) · \(date)")
}
func record(at index: Int) -> Recording? {
folder.contents[index].recording
}
}
(3)View层(ViewController,仅做UI绑定与事件转发)
import UIKit
import RxSwift
import RxCocoa
class RecordListViewController: UITableViewController {
var viewModel: RecordListViewModel!
private let disposeBag = DisposeBag()
private let loadingIndicator = UIActivityIndicatorView(style: .large)
// 导航事件:统一封装,仅发送事件,不处理跳转
struct NavigationEvent {
let didSelectRecord = PublishSubject<Recording>()
let didTapAddRecord = PublishSubject<Void>()
let didTapBack = PublishSubject<Void>()
}
let navigationEvent = NavigationEvent()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
bindNavigationTriggers()
}
private func setupUI() {
title = "录音列表"
tableView.register(ItemCell.self, forCellReuseIdentifier: "ItemCell")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
view.addSubview(loadingIndicator)
}
// 绑定UI触发的导航事件
private func bindNavigationTriggers() {
// 添加按钮点击
navigationItem.rightBarButtonItem?.rx.tap
.bind(to: navigationEvent.didTapAddRecord)
.disposed(by: disposeBag)
// 列表选中
tableView.rx.itemSelected
.compactMap { [weak self] indexPath -> Recording? in
self?.viewModel.record(at: indexPath.row)
}
.bind(to: navigationEvent.didSelectRecord)
.disposed(by: disposeBag)
// 返回按钮点击
navigationItem.leftBarButtonItem?.rx.tap
.bind(to: navigationEvent.didTapBack)
.disposed(by: disposeBag)
}
// 绑定ViewModel,无任何跳转/业务逻辑
private func bindViewModel() {
let input = RecordListViewModel.Input(
deleteTrigger: tableView.rx.itemDeleted.map { $0.row }.asObservable(),
loadTrigger: rx.viewWillAppear.map { _ in () }.asObservable()
)
let output = viewModel.transform(input: input)
// 绑定列表数据
output.records
.drive(tableView.rx.items(cellIdentifier: "ItemCell", cellType: ItemCell.self)) { [weak self] index, item, cell in
guard let self = self else { return }
let info = self.viewModel.recordDisplayInfo(for: item)
cell.nameLabel.text = info.name
cell.subtitleLabel.text = info.subtitle
}
.disposed(by: disposeBag)
// 绑定加载状态
output.isLoading
.drive(loadingIndicator.rx.isAnimating)
.disposed(by: disposeBag)
// 绑定错误提示
output.error
.filter { !$0.isEmpty }
.drive(onNext: { message in
let alert = UIAlertController(title: "错误", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true)
})
.disposed(by: disposeBag)
}
}
// 自定义Cell
class ItemCell: UITableViewCell {
let nameLabel = UILabel()
let subtitleLabel = UILabel()
// 布局代码省略
}
(4)Coordinator协调器层(本章核心扩展)
① 协调器基础协议
import UIKit
import RxSwift
import RxCocoa
// 协调器基础协议
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get }
var childCoordinators: [Coordinator] { get set }
var disposeBag: DisposeBag { get }
func start() // 流程入口方法
}
// 协议默认实现:子协调器生命周期管理
extension Coordinator {
func addChild(_ coordinator: Coordinator) {
childCoordinators.append(coordinator)
}
func removeChild(_ coordinator: Coordinator) {
childCoordinators.removeAll { $0 === coordinator }
}
}
// 可结束流程的协调器协议
protocol FlowCoordinator: Coordinator {
var flowDidFinish = PublishSubject<Void> { get }
}
② 主流程协调器(App入口)
final class AppFlowCoordinator: Coordinator {
var navigationController: UINavigationController
var childCoordinators: [Coordinator] = []
let disposeBag = DisposeBag()
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
// 启动录音列表主流程
let listCoordinator = RecordListCoordinator(
navigationController: navigationController,
folder: Store.shared.rootFolder
)
addChild(listCoordinator)
listCoordinator.start()
// 监听子流程结束,自动释放
listCoordinator.flowDidFinish
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.removeChild(listCoordinator)
})
.disposed(by: disposeBag)
}
}
③ 业务子协调器(录音列表流程)
final class RecordListCoordinator: FlowCoordinator {
var navigationController: UINavigationController
var childCoordinators: [Coordinator] = []
let disposeBag = DisposeBag()
let flowDidFinish = PublishSubject<Void>()
private let folder: Folder
private let store: Store
init(navigationController: UINavigationController, folder: Folder, store: Store = .shared) {
self.navigationController = navigationController
self.folder = folder
self.store = store
}
func start() {
// 1. 注入ViewModel依赖
let viewModel = RecordListViewModel(folder: folder, store: store)
// 2. 创建ViewController,注入ViewModel
let vc = RecordListViewController()
vc.viewModel = viewModel
// 3. 绑定VC的导航事件
bindNavigationEvents(vc: vc)
// 4. 推入导航栈
navigationController.pushViewController(vc, animated: false)
}
// 统一处理所有导航跳转
private func bindNavigationEvents(vc: RecordListViewController) {
// 跳转录音详情页
vc.navigationEvent.didSelectRecord
.subscribe(onNext: { [weak self] recording in
guard let self = self else { return }
self.navigateToDetail(recording: recording)
})
.disposed(by: disposeBag)
// 跳转新建录音页
vc.navigationEvent.didTapAddRecord
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.navigateToNewRecord()
})
.disposed(by: disposeBag)
// 页面退出,通知父协调器
vc.navigationEvent.didTapBack
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.flowDidFinish.onNext(())
})
.disposed(by: disposeBag)
}
// 跳转详情页
private func navigateToDetail(recording: Recording) {
let detailVM = RecordDetailViewModel(recording: recording, store: store)
let detailVC = RecordDetailViewController()
detailVC.viewModel = detailVM
navigationController.pushViewController(detailVC, animated: true)
}
// 跳转新建录音页(启动独立子流程)
private func navigateToNewRecord() {
let recordCoordinator = RecordFlowCoordinator(
navigationController: navigationController,
folder: folder
)
addChild(recordCoordinator)
recordCoordinator.start()
// 监听子流程结束
recordCoordinator.flowDidFinish
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.removeChild(recordCoordinator)
})
.disposed(by: disposeBag)
}
}
二、MVVM+C 标准通信机制(RxSwift版)
| 通信方向 | 合法通信方式 | 禁止行为 |
|---|---|---|
| View→ViewModel | 通过Input的Observable传入用户事件 | 直接持有/修改Model、实现业务逻辑 |
| View→Coordinator | 通过NavigationEvent的Subject发送跳转事件 | 直接push/present页面、持有其他VC |
| ViewModel→Model | 直接持有,调用Model公开方法 | 直接修改Model私有属性 |
| Model→ViewModel | Notification/KVO转为Observable | 直接持有ViewModel |
| ViewModel→View | 通过Output的Driver暴露状态 | 直接持有View对象 |
| Coordinator→View | 创建VC、注入ViewModel,订阅事件 | 持有VC强引用、修改UI |
| Coordinator之间 | 父持有子,子通过Subject通知父流程结束 | 双向持有、循环引用 |
三、全量常见误区(每个配错误+正确RxSwift代码)
(一)MVVM核心误区
误区1:ViewModel直接持有View,双向循环引用
错误示例:
// 错误:ViewModel直接持有VC,双向强引用
class RecordListViewModel {
weak var viewController: RecordListViewController?
func deleteRecord(at index: Int) {
folder.removeItem(at: index)
viewController?.tableView.reloadData() // 直接调用View方法
}
}
class RecordListViewController: UITableViewController {
var viewModel: RecordListViewModel! // 强持有ViewModel
}
正确示例:
// 正确:通过Driver暴露状态,不持有View
class RecordListViewModel {
struct Output {
let records: Driver<[AnyItem]>
}
func transform(input: Input) -> Output {
let records = NotificationCenter.default.rx.notification(Store.changedNotification)
.map { [weak self] _ in self?.folder.contents ?? [] }
.startWith(folder.contents)
.asDriver(onErrorJustReturn: [])
return Output(records: records)
}
}
class RecordListViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
let output = viewModel.transform(input: input)
output.records
.drive(tableView.rx.items) { /* 刷新UI */ }
.disposed(by: disposeBag)
}
}
误区2:ViewModel包含UI逻辑,依赖UIKit
错误示例:
// 错误:ViewModel依赖UIKit,包含UI样式逻辑
class RecordListViewModel {
func recordTextColor(for item: AnyItem) -> UIColor {
item.name.isEmpty ? .red : .black
}
}
正确示例:
// 正确:ViewModel仅返回业务状态,UI逻辑在View层
class RecordListViewModel {
func isRecordNameEmpty(for item: AnyItem) -> Bool {
item.name.isEmpty
}
}
class RecordListViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
output.records
.drive(tableView.rx.items) { [weak self] index, item, cell in
// UI逻辑在View层处理
cell.nameLabel.textColor = self?.viewModel.isRecordNameEmpty(for: item) ?? false ? .red : .black
}
.disposed(by: disposeBag)
}
}
(二)Coordinator协调器核心误区
误区3:ViewController之间直接跳转,强耦合无法复用
错误示例:
// 错误:VC内部直接push其他VC,强耦合
class RecordListViewController: UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let recording = viewModel.record(at: indexPath.row) else { return }
// VC内部直接创建并跳转,强耦合
let detailVC = RecordDetailViewController()
detailVC.recording = recording
navigationController?.pushViewController(detailVC, animated: true)
}
}
正确示例:
// 正确:VC仅发送事件,跳转完全由协调器处理
class RecordListViewController: UITableViewController {
let navigationEvent = NavigationEvent()
override func viewDidLoad() {
super.viewDidLoad()
tableView.rx.itemSelected
.compactMap { [weak self] indexPath -> Recording? in
self?.viewModel.record(at: indexPath.row)
}
.bind(to: navigationEvent.didSelectRecord) // 仅发送事件
.disposed(by: disposeBag)
}
}
// 协调器统一处理跳转
class RecordListCoordinator: FlowCoordinator {
private func bindNavigationEvents(vc: RecordListViewController) {
vc.navigationEvent.didSelectRecord
.subscribe(onNext: { [weak self] recording in
self?.navigateToDetail(recording: recording) // 协调器执行跳转
})
.disposed(by: disposeBag)
}
}
误区4:协调器与VC双向持有,循环引用无法释放
错误示例:
// 错误:双向强持有,循环引用
class RecordListViewController: UIViewController {
var coordinator: RecordListCoordinator? // VC强持有协调器
}
class RecordListCoordinator: Coordinator {
var vc: RecordListViewController? // 协调器强持有VC
func start() {
let vc = RecordListViewController()
vc.coordinator = self // 双向强持有
self.vc = vc
}
}
正确示例:
// 正确:VC不持有协调器,无循环引用
class RecordListViewController: UIViewController {
// 不持有协调器,仅暴露事件Subject
let navigationEvent = NavigationEvent()
}
class RecordListCoordinator: Coordinator {
func start() {
let vc = RecordListViewController()
// 仅订阅事件,不强持有VC
vc.navigationEvent.didSelectRecord
.subscribe(onNext: { [weak self] recording in
self?.navigateToDetail(recording: recording)
})
.disposed(by: disposeBag)
navigationController.pushViewController(vc, animated: true)
}
}
误区5:协调器包含业务逻辑,职责越权
错误示例:
// 错误:业务逻辑写在协调器中,职责混乱
class RecordListCoordinator: Coordinator {
func deleteRecord(at index: Int) {
// 业务逻辑越权
let folder = Store.shared.rootFolder
folder.contents.remove(at: index)
Store.shared.save()
}
}
正确示例:
// 正确:业务逻辑封装在Model/ViewModel中
class RecordListViewModel {
func deleteRecord(at index: Int) {
folder.removeItem(at: index)
}
}
// 协调器仅处理导航,不涉及业务逻辑
class RecordListCoordinator: Coordinator {
// 无任何业务逻辑代码
}
四、全量优化方案(每个配RxSwift代码)
(一)MVVM核心优化
优化1:用Driver优化主线程调度与错误处理
核心思路:使用Driver替代Observable,自动保证主线程调度、无错误事件、共享状态,简化UI绑定代码。 代码示例:
class RecordListViewModel {
struct Output {
let records: Driver<[AnyItem]> // Driver:主线程、无错误、自动共享
let isLoading: Driver<Bool>
}
func transform(input: Input) -> Output {
let records = NotificationCenter.default.rx.notification(Store.changedNotification)
.map { [weak self] _ in self?.folder.contents ?? [] }
.startWith(folder.contents)
.asDriver(onErrorJustReturn: []) // 错误兜底
return Output(records: records, isLoading: isLoading.asDriver())
}
}
// View中直接drive,无需手动切换主线程
class RecordListViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
output.records
.drive(tableView.rx.items) { /* 自动在主线程执行 */ }
.disposed(by: disposeBag)
}
}
优化2:拆分复杂ViewModel,按功能模块化
核心思路:将列表项的展示逻辑拆分为子ViewModel,降低主ViewModel复杂度,提升复用性。 代码示例:
// 拆分后的列表项ViewModel
class RecordItemViewModel {
private let recording: Recording
var name: String { recording.name }
var durationText: String { String(format: "%.1f秒", recording.duration) }
var dateText: String {
DateFormatter.localizedString(from: recording.createDate, dateStyle: .short, timeStyle: .short)
}
var isNameEmpty: Bool { recording.name.isEmpty }
init(recording: Recording) {
self.recording = recording
}
}
// 主ViewModel使用子ViewModel
class RecordListViewModel {
struct Output {
let itemViewModels: Driver<[RecordItemViewModel]>
}
func transform(input: Input) -> Output {
let itemViewModels = NotificationCenter.default.rx.notification(Store.changedNotification)
.map { [weak self] _ in
self?.folder.contents.compactMap { $0.recording }.map { RecordItemViewModel(recording: $0) } ?? []
}
.startWith(folder.contents.compactMap { $0.recording }.map { RecordItemViewModel(recording: $0) })
.asDriver(onErrorJustReturn: [])
return Output(itemViewModels: itemViewModels)
}
}
(二)Coordinator协调器核心优化
优化3:父子协调器拆分,按业务模块管理流程
核心思路:按业务模块拆分子协调器,避免单协调器职责臃肿,每个协调器仅管理一个独立业务流程。 代码示例:
// 独立的录音流程子协调器
final class RecordFlowCoordinator: FlowCoordinator {
var navigationController: UINavigationController
var childCoordinators: [Coordinator] = []
let disposeBag = DisposeBag()
let flowDidFinish = PublishSubject<Void>()
let recordDidSave = PublishSubject<Recording>()
private let folder: Folder
init(navigationController: UINavigationController, folder: Folder) {
self.navigationController = navigationController
self.folder = folder
}
func start() {
let viewModel = RecordViewModel(folder: folder)
let vc = RecordViewController()
vc.viewModel = viewModel
// 绑定VC事件
vc.navigationEvent.didFinishRecord
.subscribe(onNext: { [weak self] recording in
guard let self = self else { return }
self.recordDidSave.onNext(recording)
self.flowDidFinish.onNext(())
self.navigationController.dismiss(animated: true)
})
.disposed(by: disposeBag)
navigationController.present(UINavigationController(rootViewController: vc), animated: true)
}
}
// 父协调器仅管理子协调器生命周期
class RecordListCoordinator: FlowCoordinator {
private func navigateToNewRecord() {
let recordCoordinator = RecordFlowCoordinator(
navigationController: navigationController,
folder: folder
)
addChild(recordCoordinator)
recordCoordinator.start()
// 监听子流程结束,自动释放
recordCoordinator.flowDidFinish
.subscribe(onNext: { [weak self] in
self?.removeChild(recordCoordinator)
})
.disposed(by: disposeBag)
}
}
优化4:协调器统一管理依赖注入,提升可测试性
核心思路:所有ViewModel/ViewController的依赖注入,全部由协调器统一管理,无需修改VC/ViewModel代码即可注入Mock对象进行测试。 代码示例:
final class RecordListCoordinator: FlowCoordinator {
private let folder: Folder
private let store: Store
// 协调器接收依赖,统一管理
init(navigationController: UINavigationController, folder: Folder, store: Store = .shared) {
self.navigationController = navigationController
self.folder = folder
self.store = store
}
func start() {
// 统一注入ViewModel依赖
let viewModel = RecordListViewModel(folder: folder, store: store)
let vc = RecordListViewController()
vc.viewModel = viewModel
navigationController.pushViewController(vc, animated: true)
}
}
// 测试时直接注入Mock依赖
func testRecordListFlow() {
let mockFolder = MockFolder()
let mockStore = MockStore()
let navigationVC = UINavigationController()
let coordinator = RecordListCoordinator(
navigationController: navigationVC,
folder: mockFolder,
store: mockStore
)
coordinator.start()
// 测试流程逻辑
}
优化5:深层链接通过协调器统一分发
核心思路:App的外部跳转、深层链接,全部由主协调器统一分发,避免跳转逻辑散落在各处。 代码示例:
final class AppFlowCoordinator: Coordinator {
// 统一处理深层链接
func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return }
switch components.path {
case "/record/detail":
guard let recordID = components.queryItems?.first(where: { $0.name == "id" })?.value,
let uuid = UUID(uuidString: recordID),
let recording = findRecording(uuid: uuid) else { return }
let listCoordinator = childCoordinators.compactMap { $0 as? RecordListCoordinator }.first
listCoordinator?.navigateToDetail(recording: recording)
case "/record/new":
let listCoordinator = childCoordinators.compactMap { $0 as? RecordListCoordinator }.first
listCoordinator?.navigateToNewRecord()
default: break
}
}
}
// AppDelegate/SceneDelegate中调用
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
appCoordinator.handleDeepLink(url)
}
五、原书第四章最终结论
- MVVM+C架构的核心价值:通过ViewModel剥离ViewController的业务逻辑,通过Coordinator剥离ViewController的导航逻辑,彻底解决「胖ViewController」问题,实现四层职责的完全解耦。
- 架构核心优势:
- ViewController仅负责UI绑定与事件转发,代码量大幅减少,职责单一,可独立复用
- ViewModel与View完全解耦,可独立进行单元测试
- Coordinator统一管理导航流程,业务流程清晰,便于维护与扩展,支持深层链接统一分发
- 依赖注入统一管理,测试成本大幅降低
- 落地建议:
- 简单场景可使用基础MVVM,复杂业务流程必须引入协调器
- 协调器必须严格按业务模块拆分,避免单协调器职责臃肿
- 结合RxSwift的响应式特性,可大幅简化View-ViewModel-Coordinator之间的事件通信
- 严格遵守职责边界:Coordinator仅处理导航与依赖注入,绝对不能包含业务逻辑与UI逻辑
《iOS 编程之道》第五章(101-112页)【网络模块】知识点完整总结
本章核心是将网络请求(典型副作用) 完美融入单向数据流架构,通过Middleware统一管理网络请求、错误处理、缓存等,解决网络请求分散、状态不可控、错误处理混乱的问题。所有示例基于录音App场景,适配Swift 5.0+、RxSwift 6.0+语法。
5. 网络
1. 核心设计原则
- 网络请求是副作用:必须放在Middleware中处理,绝对不能污染Reducer(纯函数)
- 网络状态完全映射到State:加载中、成功、失败、无网络等状态都由Store统一管理
- 所有网络操作通过Action触发:View/ViewModel仅dispatch Action,不直接发起请求
2. 核心基础代码
(1)网络相关State扩展
import Foundation
// 网络请求通用状态
enum NetworkState: Equatable {
case idle
case loading
case success
case failure(String)
}
// 扩展录音列表State,增加网络状态
struct RecordListState: Equatable {
var records: [Recording] = []
var networkState: NetworkState = .idle // 网络状态统一管理
var selectedRecording: Recording? = nil
}
// 全局App State
struct AppState: Equatable {
var recordListState = RecordListState()
}
(2)网络相关Action
import Foundation
// 录音列表网络Action
enum RecordListAction {
case loadRecords // 触发加载
case loadRecordsSuccess([Recording]) // 加载成功
case loadRecordsFailure(String) // 加载失败
case deleteRecord(Int) // 触发删除
case deleteRecordSuccess(Int) // 删除成功
case deleteRecordFailure(String) // 删除失败
}
// 全局Action
enum AppAction {
case recordList(RecordListAction)
}
(3)网络相关Reducer(保持纯函数)
import Foundation
func recordListReducer(_ state: RecordListState, _ action: RecordListAction) -> RecordListState {
var newState = state
switch action {
case .loadRecords:
newState.networkState = .loading // 仅更新状态,不做网络请求
case .loadRecordsSuccess(let records):
newState.networkState = .success
newState.records = records
case .loadRecordsFailure(let message):
newState.networkState = .failure(message)
case .deleteRecord(let index):
newState.networkState = .loading
case .deleteRecordSuccess(let index):
newState.networkState = .success
guard index < newState.records.count else { break }
newState.records.remove(at: index)
case .deleteRecordFailure(let message):
newState.networkState = .failure(message)
}
return newState
}
func appReducer(_ state: AppState, _ action: AppAction) -> AppState {
var newState = state
switch action {
case .recordList(let recordAction):
newState.recordListState = recordListReducer(newState.recordListState, recordAction)
}
return newState
}
二、常见误区(每个配错误+正确代码)
误区1:网络请求放在Reducer中(违反纯函数原则)
错误示例:
// 错误:Reducer内部直接发起网络请求,有副作用
func recordListReducer(_ state: RecordListState, _ action: RecordListAction) -> RecordListState {
var newState = state
switch action {
case .loadRecords:
newState.networkState = .loading
// 错误:Reducer包含网络请求,违反纯函数
URLSession.shared.dataTask(with: URL(string: "https://api.example.com/records")!) { data, _, error in
if let error = error {
// 错误:Reducer内部直接dispatch,破坏单向流
Store.shared.dispatch(.recordList(.loadRecordsFailure(error.localizedDescription)))
return
}
if let data = data, let records = try? JSONDecoder().decode([Recording].self, from: data) {
Store.shared.dispatch(.recordList(.loadRecordsSuccess(records)))
}
}.resume()
default: break
}
return newState
}
正确示例:
// 正确:Reducer保持纯函数,网络请求放在Middleware
func recordListReducer(_ state: RecordListState, _ action: RecordListAction) -> RecordListState {
var newState = state
switch action {
case .loadRecords:
newState.networkState = .loading
case .loadRecordsSuccess(let records):
newState.networkState = .success
newState.records = records
case .loadRecordsFailure(let message):
newState.networkState = .failure(message)
default: break
}
return newState
}
// 网络请求在Middleware中处理
func recordNetworkMiddleware() -> Middleware<AppState> {
return { state, action, dispatch in
switch action {
case .recordList(.loadRecords):
// 拦截Action,发起网络请求
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/records")!))
.subscribe(onNext: { data in
if let records = try? JSONDecoder().decode([Recording].self, from: data) {
dispatch(.recordList(.loadRecordsSuccess(records)))
}
}, onError: { error in
dispatch(.recordList(.loadRecordsFailure(error.localizedDescription)))
})
.disposed(by: DisposeBag())
default: break
}
}
}
误区2:网络请求直接在View/ViewModel中发起,不经过Action/Store
错误示例:
// 错误:ViewModel直接发起网络请求,绕过Store/Action
class RecordListViewModel {
let records = BehaviorRelay<[Recording]>(value: [])
let isLoading = BehaviorRelay<Bool>(value: false)
let error = PublishRelay<String>()
private let disposeBag = DisposeBag()
func loadRecords() {
isLoading.accept(true)
// 直接发起请求,状态分散在ViewModel,不可追溯
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/records")!))
.subscribe(onNext: { [weak self] data in
guard let self = self else { return }
self.isLoading.accept(false)
if let records = try? JSONDecoder().decode([Recording].self, from: data) {
self.records.accept(records)
}
}, onError: { [weak self] error in
guard let self = self else { return }
self.isLoading.accept(false)
self.error.accept(error.localizedDescription)
})
.disposed(by: disposeBag)
}
}
正确示例:
// 正确:ViewModel仅dispatch Action,网络状态从Store订阅
class RecordListViewModel {
private let disposeBag = DisposeBag()
// 仅触发Action,不直接发起请求
func loadRecords() {
Store.shared.dispatch(.recordList(.loadRecords))
}
func deleteRecord(at index: Int) {
Store.shared.dispatch(.recordList(.deleteRecord(index)))
}
}
// View从Store订阅网络状态
class RecordListViewController: UITableViewController {
private let disposeBag = DisposeBag()
private let viewModel = RecordListViewModel()
override func viewDidLoad() {
super.viewDidLoad()
bindState()
viewModel.loadRecords()
}
private func bindState() {
// 所有网络状态从Store统一订阅
Store.shared.select(RecordListSelectors.records)
.drive(tableView.rx.items(cellIdentifier: "ItemCell", cellType: ItemCell.self)) { index, record, cell in
cell.nameLabel.text = record.name
}
.disposed(by: disposeBag)
Store.shared.select(RecordListSelectors.networkState)
.drive(onNext: { [weak self] state in
guard let self = self else { return }
switch state {
case .idle: break
case .loading: self.loadingIndicator.startAnimating()
case .success: self.loadingIndicator.stopAnimating()
case .failure(let message):
self.loadingIndicator.stopAnimating()
self.showError(message)
}
})
.disposed(by: disposeBag)
}
}
误区3:网络错误处理分散,没有统一映射与提示
错误示例:
// 错误:每个网络请求单独处理错误,逻辑重复且不一致
func recordNetworkMiddleware() -> Middleware<AppState> {
return { state, action, dispatch in
switch action {
case .recordList(.loadRecords):
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/records")!))
.subscribe(onNext: { data in
// 错误:错误处理逻辑重复
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 401 {
dispatch(.recordList(.loadRecordsFailure("登录已过期")))
} else if httpResponse.statusCode == 500 {
dispatch(.recordList(.loadRecordsFailure("服务器错误")))
}
}
}, onError: { error in
// 错误:错误信息直接传递,没有统一映射
dispatch(.recordList(.loadRecordsFailure(error.localizedDescription)))
})
.disposed(by: DisposeBag())
case .recordList(.deleteRecord(let index)):
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/records/\(index)")!))
.subscribe(onNext: { _ in
dispatch(.recordList(.deleteRecordSuccess(index)))
}, onError: { error in
// 错误:同样的错误处理逻辑重复写
dispatch(.recordList(.deleteRecordFailure(error.localizedDescription)))
})
.disposed(by: DisposeBag())
default: break
}
}
}
正确示例:
// 正确:统一错误映射工具
enum NetworkErrorMapper {
static func map(error: Error) -> String {
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet: return "无网络连接,请检查网络"
case .timedOut: return "请求超时,请稍后重试"
default: return "网络错误:\(urlError.localizedDescription)"
}
}
if let httpError = error as? HTTPError {
switch httpError.statusCode {
case 401: return "登录已过期,请重新登录"
case 403: return "无权限操作"
case 404: return "资源不存在"
case 500...599: return "服务器错误,请稍后重试"
default: return "请求失败:\(httpError.statusCode)"
}
}
return "未知错误:\(error.localizedDescription)"
}
}
// 自定义HTTP错误
struct HTTPError: Error {
let statusCode: Int
}
// Middleware中使用统一错误映射
func recordNetworkMiddleware() -> Middleware<AppState> {
return { state, action, dispatch in
switch action {
case .recordList(.loadRecords):
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/records")!))
.subscribe(onNext: { data in
if let records = try? JSONDecoder().decode([Recording].self, from: data) {
dispatch(.recordList(.loadRecordsSuccess(records)))
}
}, onError: { error in
// 统一错误映射
let message = NetworkErrorMapper.map(error: error)
dispatch(.recordList(.loadRecordsFailure(message)))
})
.disposed(by: DisposeBag())
case .recordList(.deleteRecord(let index)):
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.example.com/records/\(index)")!))
.subscribe(onNext: { _ in
dispatch(.recordList(.deleteRecordSuccess(index)))
}, onError: { error in
// 复用统一错误映射
let message = NetworkErrorMapper.map(error: error)
dispatch(.recordList(.deleteRecordFailure(message)))
})
.disposed(by: DisposeBag())
default: break
}
}
}
三、优化方案(每个配代码)
优化1:封装通用网络Middleware,支持请求配置与复用
核心思路:将网络请求的通用逻辑(URL构建、参数编码、Header设置)封装成通用Middleware,支持不同模块的请求配置,避免代码重复。 代码示例:
import Foundation
import RxSwift
// 网络请求配置协议
protocol NetworkRequest {
var path: String { get }
var method: HTTPMethod { get }
var parameters: [String: Any]? { get }
var headers: [String: String]? { get }
associatedtype Response: Decodable
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case delete = "DELETE"
}
// 录音列表加载请求
struct LoadRecordsRequest: NetworkRequest {
typealias Response = [Recording]
let path = "/records"
let method: HTTPMethod = .get
let parameters: [String: Any]? = nil
let headers: [String: String]? = ["Authorization": "Bearer token"]
}
// 录音删除请求
struct DeleteRecordRequest: NetworkRequest {
typealias Response = EmptyResponse
let path: String
let method: HTTPMethod = .delete
let parameters: [String: Any]? = nil
let headers: [String: String]? = ["Authorization": "Bearer token"]
init(recordId: UUID) {
path = "/records/\(recordId.uuidString)"
}
}
struct EmptyResponse: Decodable {}
// 通用网络Middleware
func genericNetworkMiddleware() -> Middleware<AppState> {
return { state, action, dispatch in
// 定义通用请求处理函数
func performRequest<T: NetworkRequest>(_ request: T, successAction: @escaping (T.Response) -> AppAction, failureAction: @escaping (String) -> AppAction) {
guard let url = URL(string: "https://api.example.com\(request.path)") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
request.headers?.forEach { urlRequest.setValue($1, forHTTPHeaderField: $0) }
URLSession.shared.rx.data(request: urlRequest)
.subscribe(onNext: { data in
if let response = try? JSONDecoder().decode(T.Response.self, from: data) {
dispatch(successAction(response))
}
}, onError: { error in
let message = NetworkErrorMapper.map(error: error)
dispatch(failureAction(message))
})
.disposed(by: DisposeBag())
}
// 处理不同Action
switch action {
case .recordList(.loadRecords):
performRequest(
LoadRecordsRequest(),
successAction: { .recordList(.loadRecordsSuccess($0)) },
failureAction: { .recordList(.loadRecordsFailure($0)) }
)
case .recordList(.deleteRecord(let index)):
guard let recordId = state.recordListState.records[index].uuid else { break }
performRequest(
DeleteRecordRequest(recordId: recordId),
successAction: { _ in .recordList(.deleteRecordSuccess(index)) },
failureAction: { .recordList(.deleteRecordFailure($0)) }
)
default: break
}
}
}
优化2:引入缓存策略(内存+磁盘),减少重复请求
核心思路:在Middleware中加入缓存层,优先从缓存读取数据,缓存过期或无缓存时再发起网络请求,提升用户体验。 代码示例:
import Foundation
// 缓存管理器
class NetworkCacheManager {
static let shared = NetworkCacheManager()
private let memoryCache = NSCache<NSString, AnyObject>()
private let diskCacheURL: URL
private init() {
diskCacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("NetworkCache")
try? FileManager.default.createDirectory(at: diskCacheURL, withIntermediateDirectories: true)
}
// 缓存数据
func cache<T: Encodable>(_ data: T, for key: String, expiresIn: TimeInterval = 300) {
let cacheItem = CacheItem(data: data, expiresAt: Date().addingTimeInterval(expiresIn))
memoryCache.setObject(cacheItem, forKey: key as NSString)
if let encoded = try? JSONEncoder().encode(cacheItem) {
try? encoded.write(to: diskCacheURL.appendingPathComponent(key))
}
}
// 读取缓存
func getCached<T: Decodable>(for key: String) -> T? {
// 优先读内存缓存
if let cacheItem = memoryCache.object(forKey: key as NSString) as? CacheItem<T> {
if cacheItem.expiresAt > Date() {
return cacheItem.data
} else {
memoryCache.removeObject(forKey: key as NSString)
}
}
// 再读磁盘缓存
let fileURL = diskCacheURL.appendingPathComponent(key)
if let data = try? Data(contentsOf: fileURL),
let cacheItem = try? JSONDecoder().decode(CacheItem<T>.self, from: data) {
if cacheItem.expiresAt > Date() {
memoryCache.setObject(cacheItem, forKey: key as NSString)
return cacheItem.data
} else {
try? FileManager.default.removeItem(at: fileURL)
}
}
return nil
}
}
// 缓存项
struct CacheItem<T: Codable>: Codable {
let data: T
let expiresAt: Date
}
// 优化Middleware,加入缓存
func cachedNetworkMiddleware() -> Middleware<AppState> {
return { state, action, dispatch in
switch action {
case .recordList(.loadRecords):
let cacheKey = "records_list"
// 优先读缓存
if let cachedRecords: [Recording] = NetworkCacheManager.shared.getCached(for: cacheKey) {
dispatch(.recordList(.loadRecordsSuccess(cachedRecords)))
return
}
// 无缓存则发起请求
performRequest(
LoadRecordsRequest(),
successAction: { records in
// 缓存成功数据
NetworkCacheManager.shared.cache(records, for: cacheKey)
return .recordList(.loadRecordsSuccess(records))
},
failureAction: { .recordList(.loadRecordsFailure($0)) }
)
default: break
}
}
}
优化3:实现请求取消机制,避免内存泄漏与多余请求
核心思路:在Middleware中管理请求的DisposeBag,当页面销毁或触发取消Action时,自动取消未完成的请求。 代码示例:
import RxSwift
// 扩展Action,增加取消请求
enum RecordListAction {
case loadRecords
case loadRecordsSuccess([Recording])
case loadRecordsFailure(String)
case cancelLoadRecords // 新增取消Action
}
// 升级Middleware,管理请求生命周期
func cancellableNetworkMiddleware() -> Middleware<AppState> {
// 按模块管理DisposeBag
var requestDisposeBags: [String: DisposeBag] = [:]
return { state, action, dispatch in
switch action {
case .recordList(.loadRecords):
let key = "record_list_load"
let disposeBag = DisposeBag()
requestDisposeBags[key] = disposeBag
performRequest(
LoadRecordsRequest(),
successAction: { .recordList(.loadRecordsSuccess($0)) },
failureAction: { .recordList(.loadRecordsFailure($0)) }
)
.disposed(by: disposeBag)
case .recordList(.cancelLoadRecords):
let key = "record_list_load"
requestDisposeBags.removeValue(forKey: key) // 自动取消请求
default: break
}
}
}
// ViewModel/View中触发取消
class RecordListViewController: UITableViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 页面消失时取消请求
Store.shared.dispatch(.recordList(.cancelLoadRecords))
}
}
四、原书第五章【网络】最终结论
- 网络模块在单向数据流中的核心定位:网络是典型的副作用,必须通过Middleware统一管理,严格与Reducer(纯函数)分离,所有网络状态必须完全映射到Store的State中。
- 核心优势:
- 网络状态可预测、可追溯:所有网络操作通过Action触发,状态变更有完整记录
- 错误处理统一:通过统一的错误映射工具,保证错误提示的一致性
- 性能优化:通过缓存、请求取消等机制,提升用户体验
- 落地建议:
- 封装通用网络Middleware,支持请求配置复用
- 严格保持Reducer纯函数特性,网络请求绝对不能出现在Reducer中
- 所有网络状态从Store统一订阅,View/ViewModel不直接管理网络状态
- 根据业务需求引入缓存、请求取消等优化机制