objc中国《App架构》学习笔记

4 阅读24分钟

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→ControllerTarget-Action、Delegate直接持有/修改Model
Model→ControllerNotification、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→ViewModelNotification、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→ViewModelNotification/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)
}

五、原书第四章最终结论

  1. MVVM+C架构的核心价值:通过ViewModel剥离ViewController的业务逻辑,通过Coordinator剥离ViewController的导航逻辑,彻底解决「胖ViewController」问题,实现四层职责的完全解耦。
  2. 架构核心优势
    • ViewController仅负责UI绑定与事件转发,代码量大幅减少,职责单一,可独立复用
    • ViewModel与View完全解耦,可独立进行单元测试
    • Coordinator统一管理导航流程,业务流程清晰,便于维护与扩展,支持深层链接统一分发
    • 依赖注入统一管理,测试成本大幅降低
  3. 落地建议
    • 简单场景可使用基础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))
    }
}

四、原书第五章【网络】最终结论

  1. 网络模块在单向数据流中的核心定位:网络是典型的副作用,必须通过Middleware统一管理,严格与Reducer(纯函数)分离,所有网络状态必须完全映射到Store的State中。
  2. 核心优势
    • 网络状态可预测、可追溯:所有网络操作通过Action触发,状态变更有完整记录
    • 错误处理统一:通过统一的错误映射工具,保证错误提示的一致性
    • 性能优化:通过缓存、请求取消等机制,提升用户体验
  3. 落地建议
    • 封装通用网络Middleware,支持请求配置复用
    • 严格保持Reducer纯函数特性,网络请求绝对不能出现在Reducer中
    • 所有网络状态从Store统一订阅,View/ViewModel不直接管理网络状态
    • 根据业务需求引入缓存、请求取消等优化机制