构建可测试且可回滚的状态管理(State Management),核心在于将状态变更逻辑与副作用执行彻底分离。在 Combine 中,这通常通过实现一个基于“单向数据流”的 Store 来完成。
以下是实现这一目标的四个关键步骤:
1. 定义不可变的 State 与 Action
首先,状态必须是纯数据结构(Value Type) ,而变更意图必须是枚举(Enum) 。这是实现“回滚”的前提。
Swift
struct AppState: Equatable {
var count: Int = 0
var history: [Int] = [] // 用于存储历史记录
}
enum AppAction {
case increment
case decrement
case undo // 回滚操作
}
2. 构建纯函数 Reducer
Reducer 是状态管理的“大脑”。它是一个纯函数,输入当前状态和动作,输出新状态。因为它不依赖外部环境,所以它是 100% 可测试的。
Swift
func appReducer(state: inout AppState, action: AppAction) {
switch action {
case .increment:
state.history.append(state.count)
state.count += 1
case .decrement:
state.history.append(state.count)
state.count -= 1
case .undo:
if let lastCount = state.history.popLast() {
state.count = lastCount
}
}
}
3. 使用 CurrentValueSubject 封装 Store
Store 是连接视图与逻辑的桥梁。我们利用 CurrentValueSubject 的持久化特性来持有状态,并通过 scan 操作符来模拟状态的连续演进。
Swift
final class Store: ObservableObject {
@Published private(set) var state: AppState
private let actions = PassthroughSubject<AppAction, Never>()
private var cancellables = Set<AnyCancellable>()
init(initialState: AppState) {
self.state = initialState
// 核心逻辑:使用 scan 将 Action 流转换为连续的 State 流
actions
.scan(initialState) { currentState, action in
var newState = currentState
appReducer(state: &newState, action: action)
return newState
}
.receive(on: DispatchQueue.main)
.assign(to: &$state)
}
func send(_ action: AppAction) {
actions.send(action)
}
}
4. 实现可测试性与回滚逻辑
如何测试?
由于逻辑都在 appReducer 这个纯函数里,测试变得极其简单,甚至不需要模拟(Mock)对象。
Swift
func testIncrement() {
var state = AppState(count: 0)
appReducer(state: &state, action: .increment)
XCTAssertEqual(state.count, 1)
XCTAssertEqual(state.history, [0])
}
如何回滚?
在上面的代码中,我们通过 state.history 数组实现了简单的撤销。
- 深层次回滚:如果需要回滚到任意时间点,可以利用
scan记录[AppState]的完整切片。 - 时光倒流调试:你可以通过手动给
CurrentValueSubject.value赋值一个旧的AppState对象,UI 会立刻同步回滚,因为视图是声明式绑定的。
5. 高级进阶:引入副作用(Effect)
对于网络请求等副作用,可以参照 TCA 架构,让 Reducer 返回一个 AnyPublisher<AppAction, Never>。
Swift
// 在 Store 中处理副作用
actions
.flatMap { action in
// 根据 action 返回对应的副作用流
return self.effectProvider(action)
}
.sink { [weak self] nextAction in
self?.send(nextAction) // 结果作为新 Action 喂回系统
}
.store(in: &cancellables)
总结策略
| 需求 | Combine 实现方案 |
|---|---|
| 状态一致性 | 使用 CurrentValueSubject 确保任何时刻都有唯一状态 |
| 逻辑隔离 | 将变更逻辑抽离为纯函数 Reducer |
| 可测试性 | 针对 Reducer 进行单元测试,无需生命周期干扰 |
| 回滚能力 | 利用 scan 累积状态或在 State 中维护 history 栈 |
| 性能优化 | 在视图层使用 .removeDuplicates() 避免无效刷新 |