iOS第四十二篇:MVVM设计模式详解
各层职责组件职责特点Model数据获取/业务逻辑与UI完全解耦ViewModel数据转换/状态管理不引用UIKitView界面展示/用户交互被动响应状态与MVC的对比架构演变对比#mermaid-svg-MiLMWToXHR006Ard {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#m
·
# MVVM设计模式深度解析
## 目录
1. [MVVM核心思想](#mvvm核心思想)
2. [与MVC的对比](#与mvc的对比)
3. [数据绑定实现](#数据绑定实现)
4. [标准实现步骤](#标准实现步骤)
5. [依赖管理](#依赖管理)
6. [测试策略](#测试策略)
7. [实战案例](#实战案例)
8. [最佳实践](#最佳实践)
---
## MVVM核心思想
### 组件关系图
```mermaid
graph LR
A[Model] -->|数据| B[ViewModel]
B -->|状态| C[View]
C -->|用户操作| B
各层职责
组件 | 职责 | 特点 |
---|---|---|
Model | 数据获取/业务逻辑 | 与UI完全解耦 |
ViewModel | 数据转换/状态管理 | 不引用UIKit |
View | 界面展示/用户交互 | 被动响应状态 |
与MVC的对比
架构演变对比
量化对比
指标 | MVC | MVVM | 提升 |
---|---|---|---|
控制器行数 | 800+ | 200-300 | ↓70% |
可测试性 | 困难 | 简单 | ↑300% |
数据流清晰度 | 一般 | 优秀 | ↑200% |
学习曲线 | 平缓 | 较陡 | - |
数据绑定实现
基础绑定方案
// 1. 使用闭包回调
class Observable<T> {
var value: T {
didSet { listener?(value) }
}
private var listener: ((T) -> Void)?
init(_ value: T) {
self.value = value
}
func bind(_ listener: @escaping (T) -> Void) {
listener(value)
self.listener = listener
}
}
// 在ViewModel中使用
class UserViewModel {
var username = Observable("")
}
// 在View中绑定
viewModel.username.bind { [weak self] text in
self?.usernameLabel.text = text
}
高级绑定方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Closure | 轻量简单 | 手动管理 | 小型项目 |
Combine | 原生支持 | iOS13+ | Apple生态 |
RxSwift | 功能强大 | 学习曲线陡 | 复杂数据流 |
标准实现步骤
1. 定义Model
struct User {
let id: Int
let name: String
let email: String
}
2. 创建ViewModel
class UserViewModel {
// 输入
let fetchData = PassthroughSubject<Void, Never>()
// 输出
@Published var isLoading = false
@Published var user: User?
@Published var error: Error?
private let service: UserService
private var cancellables = Set<AnyCancellable>()
init(service: UserService = .shared) {
self.service = service
fetchData
.handleEvents(receiveOutput: { [weak self] _ in
self?.isLoading = true
})
.flatMap { service.fetchUser() }
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.error = error
}
},
receiveValue: { [weak self] user in
self?.user = user
}
)
.store(in: &cancellables)
}
}
3. 实现View
class UserViewController: UIViewController {
private let viewModel: UserViewModel
private var cancellables = Set<AnyCancellable>()
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
init(viewModel: UserViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
viewModel.fetchData.send()
}
private func setupBindings() {
viewModel.$user
.receive(on: DispatchQueue.main)
.sink { [weak self] user in
self?.nameLabel.text = user?.name
}
.store(in: &cancellables)
viewModel.$isLoading
.assign(to: \.isVisible, on: activityIndicator)
.store(in: &cancellables)
}
}
依赖管理
依赖注入模式
// 协议抽象
protocol UserServiceProtocol {
func fetchUser() -> AnyPublisher<User, Error>
}
// 实现
class UserService: UserServiceProtocol {
func fetchUser() -> AnyPublisher<User, Error> {
// 网络请求实现
}
}
// ViewModel通过协议依赖
class UserViewModel {
private let service: UserServiceProtocol
init(service: UserServiceProtocol) {
self.service = service
}
}
// 构造时注入
let service = UserService()
let viewModel = UserViewModel(service: service)
依赖注入容器
class DIContainer {
static let shared = DIContainer()
private init() {}
lazy var userService: UserServiceProtocol = UserService()
lazy var imageService: ImageServiceProtocol = ImageService()
func makeUserViewModel() -> UserViewModel {
UserViewModel(service: userService)
}
}
测试策略
ViewModel单元测试
class UserViewModelTests: XCTestCase {
var viewModel: UserViewModel!
var mockService: MockUserService!
override func setUp() {
super.setUp()
mockService = MockUserService()
viewModel = UserViewModel(service: mockService)
}
func testFetchSuccess() {
// 准备
let expectedUser = User(id: 1, name: "Test", email: "test@example.com")
mockService.stubUser = expectedUser
// 期望
let expectation = XCTestExpectation(description: "Fetch user")
// 绑定
viewModel.$user
.dropFirst() // 忽略初始值
.sink { user in
if user != nil {
XCTAssertEqual(user?.name, "Test")
expectation.fulfill()
}
}
.store(in: &cancellables)
// 执行
viewModel.fetchData.send()
// 验证
wait(for: [expectation], timeout: 1)
}
}
测试覆盖率目标
组件 | 覆盖率目标 | 测试类型 |
---|---|---|
ViewModel | 95%+ | 单元测试 |
Model | 100% | 单元测试 |
Service | 90%+ | 单元测试+集成测试 |
实战案例
登录页面实现
关键代码实现
// ViewModel
class LoginViewModel {
// 输入
let username = CurrentValueSubject<String, Never>("")
let password = CurrentValueSubject<String, Never>("")
let loginTapped = PassthroughSubject<Void, Never>()
// 输出
@Published var isLoading = false
@Published var loginSuccess = false
@Published var errorMessage: String?
// 验证逻辑
var isValid: AnyPublisher<Bool, Never> {
Publishers.CombineLatest(username, password)
.map { !$0.0.isEmpty && $0.1.count >= 6 }
.eraseToAnyPublisher()
}
// 登录逻辑
func login() {
// 网络请求实现
}
}
最佳实践
设计原则
-
单向数据流:
View → ViewModel → Model Model → ViewModel → View
-
View被动原则:
// 错误做法 view.tableView.reloadData() // 正确做法 viewModel.$items .assign(to: \.items, on: view)
-
ViewModel纯逻辑:
// 错误:包含UIKit引用 let label = UILabel() // 正确:只处理数据 let title = CurrentValueSubject<String, Never>("")
性能优化
技巧 | 效果 | 代码示例 |
---|---|---|
数据去重 | 减少刷新 | .removeDuplicates() |
延迟加载 | 减少内存 | lazy var data = ... |
批量操作 | 减少更新 | collectionView.performBatchUpdates |
MVVM适用性评估
迁移路线图
- 抽离业务逻辑到Service层
- 创建ViewModel处理展示逻辑
- 将ViewController转为View角色
- 实现数据绑定
- 逐步迁移各模块
在Apple平台,Combine框架是MVVM的理想搭档
截止2023年,80%的SwiftUI项目天然采用MVVM模式
这篇MVVM详解包含:
1. 从理论到实践的完整路径
2. 多种数据绑定实现方案
3. 依赖注入的最佳实践
4. 可落地的测试策略
5. 完整实战案例
6. 避免常见陷阱的技巧
建议实施策略:
- 新项目:直接采用MVVM+Combine
- 老项目:逐步迁移核心模块
- 混合架构:MVC与MVVM共存
- 学习路径:先掌握基础绑定再深入响应式编程
更多推荐
所有评论(0)