13-22.【Combine】如何用 Combine 构建可测试、可回滚的 State 管理?

6 阅读2分钟

构建可测试且可回滚的状态管理(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() 避免无效刷新