我们来详细讲解 在 UIKit 中实现 MVVM 的设计模式。这与 SwiftUI 中的 MVVM 核心思想一致,但由于 UIKit 是命令式框架,实现方式上有所不同。


1. 核心思想回顾

MVVM 的核心永远是分离关注点

  • Model: 数据和业务逻辑。
  • View: UI 呈现(UIView, UIViewController)。
  • ViewModel: 将 Model 转换为 View 可显示的状态,并处理 View 的交互逻辑。

在 UIKit 中,UIViewController及其管理的 UIView共同被视为 “V” (View层)


2. 各组件职责(UIKit 版)

组件 职责 具体任务
Model 数据与业务规则 - 数据模型 (Struct/Class) - 网络请求 (NetworkService) - 数据库操作 (PersistenceService)
View 显示UI & 用户交互 - UIViewControllerUIView - 负责创建和布局UI组件 - 通过 委托、IBAction、Target-Action 接收用户输入 - 监听 ViewModel 的状态变化并更新UI
ViewModel 状态管理与逻辑处理 - 暴露 属性和命令 供 View 绑定 - 处理来自 View 的用户输入 - 调用 Model 层进行数据获取和处理 - 将 Model 的数据转换为 View 易于使用的格式

3. 关键技术:数据绑定 (Data Binding)

由于 UIKit 没有 SwiftUI 那样的内置响应式机制,实现 数据绑定 是 UIKit MVVM 的核心挑战。主要有以下几种方式:

方式一:使用 Combine 框架 (iOS 13+)【现代推荐】

这是最接近 SwiftUI 体验的方式。ViewModel 使用 @Published属性,View 使用 sink来订阅。

ViewModel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
// SearchViewViewModel.swift
// TwitterClone
//
// Created by Amr Hossam on 10/03/2024.
// 数据转换为view层直接可用的格式,viewmodel的职责之一

import Foundation
import Combine

class SearchViewViewModel {

var subscriptions: Set<AnyCancellable> = []

func search(with query: String, _ completion: @escaping ([TwitterUser]) -> Void) {
DatabaseManager.shared.collectionUsers(search: query)
.sink { completion in
if case .failure(let error) = completion {
print(error.localizedDescription)
}
} receiveValue: { users in
completion(users)
}
.store(in: &subscriptions)
}
}

View (UIViewController):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
 //
// SearchViewController.swift
// TwitterClone
//
// Created by Amr Hossam on 22/02/2022.
//

import UIKit

class SearchViewController: UIViewController {

private let searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: SearchResultsViewController())
searchController.searchBar.searchBarStyle = .minimal
searchController.searchBar.placeholder = "Search with @usernames"
return searchController
}()

private let promptLabel: UILabel = {
let label = UILabel()
label.text = "Search for users and get connected"
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 32, weight: .bold)
label.textColor = .placeholderText
return label
}()

let viewModel: SearchViewViewModel

init(viewModel: SearchViewViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(promptLabel)
navigationItem.searchController = searchController
searchController.searchResultsUpdater = self
configureConstraints()
}

private func configureConstraints() {
let promptLabelConstraints = [
promptLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
promptLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
promptLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
]

NSLayoutConstraint.activate(promptLabelConstraints)
}
}

extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let resultsViewController = searchController.searchResultsController as? SearchResultsViewController,
let query = searchController.searchBar.text
else { return }
viewModel.search(with: query) { users in
resultsViewController.update(users: users)
}
}
}

3. 完整交互流程(以 Combine 为例)

4. UIKit MVVM 的优势与挑战

优势:

完整交互流程

  • 极高的可测试性:ViewModel 不导入 UIKit,可轻松进行单元测试。
  • 更清晰的代码结构:将庞大的 UIViewController中的逻辑剥离出来,使其不再成为 “Massive View Controller”。
  • 更好的复用性:一个 ViewModel 可以被多个 ViewController 使用(例如 iPhone 和 iPad 的不同布局)。

挑战:

  • 需要手动绑定:需要借助 Combine、RxSwift 或闭包等方式实现绑定,有一定学习成本。
  • 入门门槛稍高:相对于传统的 MVC,需要更好地理解响应式编程概念。

总结

在 UIKit 中实践 MVVM 是提升代码质量的重要手段。Combine 框架是目前最优雅和现代的实现方式,它极大地简化了绑定过程。通过这种模式,你可以构建出更易于测试、维护和扩展的 UIKit 应用。