11-19.【错误处理】在 Combine / SwiftUI / TCA 中,如何优雅处理错误流并支持可测试恢复?

4 阅读3分钟

在响应式框架中,错误处理的优雅程度取决于你如何将“异常”转化为“状态”。核心目标是将不可控的 Error 转换为可预测的 ActionView State

以下是针对三个主流框架的防御式设计与测试策略:


1. Combine: 强类型流与错误转换

Combine 的 Publisher 要求定义 Failure 类型。优雅处理的关键在于尽早将 Error 映射为业务枚举

策略:使用 mapErrorcatch

  • 防御点:不要让底层的 URLError 渗透到 UI 层。使用 mapError 进行类型收窄。
  • 优雅恢复:使用 catch 在发生错误时切换到备用流(例如从缓存读取数据),保证流不会因错误而彻底终止。

Swift

func fetchData() -> AnyPublisher<Data, AppError> {
    URLSession.shared.dataTaskPublisher(for: url)
        .map(.data)
        .mapError { AppError.network($0) } // 统一错误类型
        .catch { error in
            // 如果报错,尝试从本地磁盘恢复
            return self.localCache.publisher() 
        }
        .eraseToAnyPublisher()
}

2. SwiftUI: 错误状态驱动的 UI

SwiftUI 的核心是状态驱动。处理错误的最佳实践是使用一个枚举状态机,而不是散乱的布尔值。

策略:枚举状态机

  • 防御点:避免同时出现 isLoading = trueerror != nil 的矛盾状态。
  • 可测试性:由于状态是确定的枚举,你可以通过简单的单元测试验证 UI 逻辑。

Swift

enum LoadingState<Value, Error: LocalizedError> {
    case idle
    case loading
    case success(Value)
    case failure(Error)
}

struct DataView: View {
    @State var state: LoadingState<User, AppError> = .idle
    
    var body: some View {
        switch state {
        case .loading: ProgressView()
        case .success(let user): Text(user.name)
        case .failure(let error):
            VStack {
                Text(error.errorDescription ?? "未知错误")
                Button("重试") { retryTask() } // 支持恢复
            }
        default: EmptyView()
        }
    }
}

3. TCA (The Composable Architecture): 纯函数式错误处理

TCA 将错误处理提升到了架构级的高度。错误被视为一种 Action

策略:catchToEffect 与依赖注入

  • 防御点:所有的副作用(如 API 调用)都封装在 Dependency 中。
  • 可测试恢复:TCA 的 TestStore 允许你模拟失败的 Effect,并验证 Reducer 是否正确处理了错误 Action 并更新了状态。

Swift

// Reducer 内部
case .fetchResponse(.failure(let error)):
    state.isLoading = false
    state.alert = AlertState(title: { TextState(error.localizedDescription) })
    return .none

case .retryButtonTapped:
    return .send(.fetchRequest) // 逻辑清晰的恢复触发

4. 可测试恢复(Testable Recovery)的最佳实践

无论使用哪个框架,支持可测试恢复的共同点是依赖抽象

A. 模拟(Mocking)错误

不要在测试中真的断开网线。通过协议(Protocol)注入 Mock Service,使其返回 .failure

Swift

struct MockService: SearchServiceProtocol {
    var errorToThrow: AppError?
    func search() async throws -> [Item] {
        if let error = errorToThrow { throw error }
        return [Item.mock]
    }
}

B. 验证恢复逻辑

在测试用例中:

  1. 注入会失败的 Mock。
  2. 触发操作,验证 State 变为 .failure
  3. 动态更换 Mock 为成功状态(或修改其行为)。
  4. 触发“重试” Action,验证 State 最终变为 .success

总结:防御式处理清单

框架核心工具恢复手段
Combinecatch, retry(n)切换 Publisher
SwiftUIenum State状态触发重绘
TCAEffect.run, AlertState发送重试 Action

设计建议:始终为你的错误视图提供一个显式的“重试”路径。这不仅提升了用户体验,也迫使你在架构层考虑如何通过重新订阅流或重新触发 Action 来恢复状态一致性。