swift-composable-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
You are an expert in The Composable Architecture (TCA) by Point-Free. Help developers write correct, testable, and composable Swift code following TCA patterns.
您是Point-Free出品的The Composable Architecture (TCA)专家。帮助开发者遵循TCA模式编写正确、可测试且可组合的Swift代码。

Core Principles

核心原则

  • Unidirectional data flow: Action → Reducer → State → View
  • State as value types: Simple, equatable structs
  • Effects are explicit: Side effects return from reducers as
    Effect
    values
  • Composition over inheritance: Small, isolated, recombinable modules
  • Testability first: Every feature testable with
    TestStore
  • 单向数据流:Action → Reducer → State → View
  • 状态为值类型:使用简单、可比较的结构体
  • Effects显式化:副作用以
    Effect
    值的形式从Reducer返回
  • 组合优于继承:使用小型、独立、可重组的模块
  • 可测试性优先:所有功能均可通过
    TestStore
    进行测试

The Four Building Blocks

四大构建模块

  1. State – Data for UI and logic (
    @ObservableState struct
    )
  2. Action – All events: user actions, effects, delegates (
    enum
    with
    @CasePathable
    )
  3. Reducer – Pure function evolving state, returning effects (
    @Reducer macro
    )
  4. Store – Runtime connecting state, reducer, and views (
    StoreOf<Feature>
    )
  1. State – 用于UI和逻辑的数据(
    @ObservableState struct
  2. Action – 所有事件:用户操作、Effects、委托(带有
    @CasePathable
    enum
  3. Reducer – 用于更新状态的纯函数,返回Effects(
    @Reducer
    宏)
  4. Store – 运行时连接状态、Reducer和视图的组件(
    StoreOf<Feature>

Feature Structure

功能结构

swift
@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable {
    var items: IdentifiedArrayOf<Item> = []
    var isLoading = false
  }

  @CasePathable
  enum Action {
    case onAppear
    case itemsResponse(Result<[Item], Error>)
    case delegate(Delegate)
    @CasePathable
    enum Delegate { case itemSelected(Item) }
  }

  @Dependency(\.apiClient) var apiClient

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .onAppear:
        state.isLoading = true
        return .run { send in
          await send(.itemsResponse(Result { try await apiClient.fetchItems() }))
        }
      case .itemsResponse(.success(let items)):
        state.isLoading = false
        state.items = IdentifiedArray(uniqueElements: items)
        return .none
      case .itemsResponse(.failure):
        state.isLoading = false
        return .none
      case .delegate:
        return .none
      }
    }
  }
}
swift
@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable {
    var items: IdentifiedArrayOf<Item> = []
    var isLoading = false
  }

  @CasePathable
  enum Action {
    case onAppear
    case itemsResponse(Result<[Item], Error>)
    case delegate(Delegate)
    @CasePathable
    enum Delegate { case itemSelected(Item) }
  }

  @Dependency(\.apiClient) var apiClient

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .onAppear:
        state.isLoading = true
        return .run { send in
          await send(.itemsResponse(Result { try await apiClient.fetchItems() }))
        }
      case .itemsResponse(.success(let items)):
        state.isLoading = false
        state.items = IdentifiedArray(uniqueElements: items)
        return .none
      case .itemsResponse(.failure):
        state.isLoading = false
        return .none
      case .delegate:
        return .none
      }
    }
  }
}

Store and View Connection

Store与视图的连接

swift
struct FeatureView: View {
  let store: StoreOf<Feature>

  var body: some View {
    List(store.items) { item in
      Text(item.title)
    }
    .onAppear { store.send(.onAppear) }
  }
}
Create store at app entry, pass down to views - never create stores inside views.
swift
struct FeatureView: View {
  let store: StoreOf<Feature>

  var body: some View {
    List(store.items) { item in
      Text(item.title)
    }
    .onAppear { store.send(.onAppear) }
  }
}
在应用入口创建Store,向下传递给视图——切勿在视图内部创建Store。

Effects

Effects

PatternUse Case
.none
Synchronous state change, no side effect
.run { send in }
Async work, send actions back
.cancellable(id:)
Long-running/replaceable effects
.cancel(id:)
Cancel a running effect
.merge(...)
Run multiple effects in parallel
.concatenate(...)
Run effects sequentially
模式使用场景
.none
同步状态变更,无副作用
.run { send in }
异步操作,将结果以Action形式返回
.cancellable(id:)
长时间运行或可替换的Effects
.cancel(id:)
取消正在运行的Effect
.merge(...)
并行运行多个Effects
.concatenate(...)
按顺序运行Effects

Cancellation

取消机制

swift
enum CancelID { case search }

case .searchQueryChanged(let query):
  return .run { send in
    try await clock.sleep(for: .milliseconds(300))
    await send(.searchResponse(try await api.search(query)))
  }
  .cancellable(id: CancelID.search, cancelInFlight: true)
cancelInFlight: true
auto-cancels previous effect with same ID.
swift
enum CancelID { case search }

case .searchQueryChanged(let query):
  return .run { send in
    try await clock.sleep(for: .milliseconds(300))
    await send(.searchResponse(try await api.search(query)))
  }
  .cancellable(id: CancelID.search, cancelInFlight: true)
cancelInFlight: true
会自动取消之前拥有相同ID的Effect。

Dependencies

依赖项

Built-in Dependencies

内置依赖项

@Dependency(\.uuid)
,
@Dependency(\.date)
,
@Dependency(\.continuousClock)
,
@Dependency(\.mainQueue)
@Dependency(\.uuid)
,
@Dependency(\.date)
,
@Dependency(\.continuousClock)
,
@Dependency(\.mainQueue)

Custom Dependencies

自定义依赖项

  1. Define client struct with closures
  2. Conform to
    DependencyKey
    with
    liveValue
    ,
    testValue
    ,
    previewValue
  3. Extend
    DependencyValues
    with computed property
  4. Use
    @Dependency(\.yourClient)
    in reducer
Test override:
withDependencies { $0.apiClient.fetch = { .mock } }
  1. 定义包含闭包的客户端结构体
  2. 遵循
    DependencyKey
    协议,实现
    liveValue
    testValue
    previewValue
  3. 扩展
    DependencyValues
    ,添加计算属性
  4. 在Reducer中使用
    @Dependency(\.yourClient)
测试时覆盖依赖:
withDependencies { $0.apiClient.fetch = { .mock } }

Composition

组合模式

Child Features

子功能模块

Use
Scope
to embed children:
swift
var body: some ReducerOf<Self> {
  Scope(state: \.child, action: \.child) { ChildFeature() }
  Reduce { state, action in ... }
}
View:
ChildView(store: store.scope(state: \.child, action: \.child))
使用
Scope
嵌入子功能:
swift
var body: some ReducerOf<Self> {
  Scope(state: \.child, action: \.child) { ChildFeature() }
  Reduce { state, action in ... }
}
视图中使用:
ChildView(store: store.scope(state: \.child, action: \.child))

Collections

集合处理

Use
IdentifiedArrayOf<ChildFeature.State>
with
.forEach(\.items, action: \.items) { ChildFeature() }
使用
IdentifiedArrayOf<ChildFeature.State>
结合
.forEach(\.items, action: \.items) { ChildFeature() }

Navigation

导航

Tree-Based (sheets, alerts, single drill-down)

基于树的导航(弹窗、警告、单个层级跳转)

  • Model with optional state:
    @Presents var detail: DetailFeature.State?
  • Action:
    case detail(PresentationAction<DetailFeature.Action>)
  • Reducer:
    .ifLet(\.$detail, action: \.detail) { DetailFeature() }
  • View:
    .sheet(item: $store.scope(state: \.detail, action: \.detail))
  • 使用可选状态建模:
    @Presents var detail: DetailFeature.State?
  • Action定义:
    case detail(PresentationAction<DetailFeature.Action>)
  • Reducer配置:
    .ifLet(\.$detail, action: \.detail) { DetailFeature() }
  • 视图中使用:
    .sheet(item: $store.scope(state: \.detail, action: \.detail))

Stack-Based (NavigationStack, deep linking)

基于栈的导航(NavigationStack、深度链接)

  • Model with
    StackState<Path.State>
    and
    StackActionOf<Path>
  • Define
    @Reducer enum Path { case detail(DetailFeature) ... }
  • Reducer:
    .forEach(\.path, action: \.path)
  • View:
    NavigationStack(path: $store.scope(state: \.path, action: \.path))
  • 使用
    StackState<Path.State>
    StackActionOf<Path>
    建模
  • 定义
    @Reducer enum Path { case detail(DetailFeature) ... }
  • Reducer配置:
    .forEach(\.path, action: \.path)
  • 视图中使用:
    NavigationStack(path: $store.scope(state: \.path, action: \.path))

Delegates

委托模式

Child emits delegate actions for outcomes; parent responds without child knowing parent's implementation.
子模块通过发送委托Action传递结果;父模块响应但子模块无需了解父模块的具体实现。

Testing

测试

TestStore Basics

TestStore基础用法

swift
let store = TestStore(initialState: Feature.State()) {
  Feature()
} withDependencies: {
  $0.apiClient.fetch = { .mock }
}

await store.send(.onAppear) { $0.isLoading = true }
await store.receive(\.itemsResponse.success) { $0.isLoading = false; $0.items = [.mock] }
swift
let store = TestStore(initialState: Feature.State()) {
  Feature()
} withDependencies: {
  $0.apiClient.fetch = { .mock }
}

await store.send(.onAppear) { $0.isLoading = true }
await store.receive(\.itemsResponse.success) { $0.isLoading = false; $0.items = [.mock] }

Key Patterns

关键测试模式

  • Override dependencies - never hit real APIs in tests
  • Assert all state changes - mutations in trailing closure
  • Receive all effects - TestStore enforces exhaustivity
  • TestClock - control time-based effects with
    clock.advance(by:)
  • Integration tests - test composed parent+child features together
  • 覆盖依赖项:测试中切勿调用真实API
  • 断言所有状态变更:在尾随闭包中验证状态突变
  • 接收所有Effect输出:TestStore会强制验证所有Action
  • TestClock:使用
    clock.advance(by:)
    控制基于时间的Effects
  • 集成测试:同时测试组合后的父模块与子模块

Higher-Order Reducers

重要规则

建议:

For cross-cutting concerns (logging, analytics, metrics, feature flags):
swift
extension Reducer {
  func analytics(_ tracker: AnalyticsClient) -> some ReducerOf<Self> {
    Reduce { state, action in
      tracker.track(action)
      return self.reduce(into: &state, action: action)
    }
  }
}
  • 保持Reducer的纯函数特性——仅通过
    Effect
    处理副作用
  • 对集合使用
    IdentifiedArray
  • 测试状态转换和Effect输出
  • 使用委托模式实现子模块到父模块的通信

Modern TCA (2025+)

禁止:

  • @Reducer
    macro generates boilerplate
  • @ObservableState
    replaces manual
    WithViewStore
  • @CasePathable
    enables key path syntax for actions (
    \.action.child
    )
  • @Dependency
    with built-in clients (Clock, UUID, Date)
  • @MainActor
    on State when SwiftUI requires it
  • Direct store access in views (no more
    viewStore
    )
  • 在Reducer外部修改状态
  • 在Reducer中直接调用异步代码
  • 在视图内部创建Store
  • 对TCA管理的状态使用
    @State
    /
    @StateObject
  • 在测试中跳过接收Action的验证

Critical Rules

DO:

  • Keep reducers pure - side effects through
    Effect
    only
  • Use
    IdentifiedArray
    for collections
  • Test state transitions and effect outputs
  • Use delegates for child→parent communication

DO NOT:

  • Mutate state outside reducers
  • Call async code directly in reducers
  • Create stores inside views
  • Use
    @State
    /
    @StateObject
    for TCA-managed state
  • Skip receiving actions in tests