我们来详细讲解 在 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 & 用户交互 |
- UIViewController 和 UIView - 负责创建和布局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
|
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
|
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 应用。