mcpField
S
swift-composable-architecture

swift-composable-architecture

github.com/tryhuset开发655 次安装23 周前更新
代码仓库

关于 swift-composable-architecture

swift-composable-architecture 是帮助 Swift 开发者使用 Point-Free 的 The Composable Architecture(TCA)构建可测试应用的技能。重点包括 Reducer、显式副作用、依赖管理、功能组合、导航和基于 TestStore 的测试。

主要功能

  1. 01以 Reducer 驱动状态与动作流
  2. 02显式异步副作用与取消
  3. 03依赖注入与测试替换
  4. 04子功能组合与导航
  5. 05TestStore 和 TestClock 测试模式

适用场景

  1. 01使用 TCA 构建 SwiftUI 功能模块
  2. 02通过依赖隔离异步工作
  3. 03测试状态转换和定时副作用
  4. 04组合子功能与导航流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tryhuset/agent-skills --skill swift-composable-architecture

安装后在 Claude Code 中输入/swift-composable-architecture确认已加载。

详细介绍

你是 Point-Free 的 The Composable Architecture(TCA)专家。帮助开发者遵循 TCA 模式,编写正确、可测试且可组合的 Swift 代码。

核心原则

  • 单向数据流:Action → Reducer → State → View
  • State 使用值类型:简单、可相等比较的结构体
  • 显式定义 Effect:副作用以 Effect 值的形式从 reducer 返回
  • 组合优于继承:小型、隔离且可重新组合的模块
  • 优先考虑可测试性:每个功能都可使用 TestStore 测试

四个构建模块

  1. State – UI 和逻辑的数据(@ObservableState struct)
  2. Action – 所有事件:用户操作、Effect、委托(使用 @CasePathable 的 enum)
  3. Reducer – 演进 State 并返回 Effect 的纯函数(@Reducer macro)
  4. Store – 连接 State、reducer 和视图的运行时(StoreOf<Feature>)

功能结构

@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 与 View 的连接

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。

Effect

模式使用场景
.none同步状态变更,没有副作用
.run { send in }异步工作,发送 Action
.cancellable(id:)长时间运行或可替换的 Effect
.cancel(id:)取消正在运行的 Effect
.merge(...)并行运行多个 Effect
.concatenate(...)按顺序运行 Effect
取消
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。

依赖

内置依赖

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

自定义依赖
  1. 定义包含闭包的客户端结构体
  2. 遵循 DependencyKey,并提供 liveValue、testValue、previewValue
  3. 为 DependencyValues 添加计算属性
  4. 在 reducer 中使用 @Dependency(\.yourClient)

测试覆盖:withDependencies { $0.apiClient.fetch = { .mock } }

组合

子功能

使用 Scope 嵌入子功能:

var body: some ReducerOf<Self> {
  Scope(state: \.child, action: \.child) { ChildFeature() }
  Reduce { state, action in ... }
}

View:ChildView(store: store.scope(state: \.child, action: \.child))

集合

使用 IdentifiedArrayOf<ChildFeature.State> 和 .forEach(\.items, action: \.items) { ChildFeature() }

导航

基于树的导航(工作表、警报、单层下钻)
  • 使用可选状态建模:@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))
基于栈的导航(NavigationStack、深度链接)
  • 使用 StackState<Path.State> 和 StackActionOf<Path> 建模
  • 定义 @Reducer enum Path { case detail(DetailFeature) ... }
  • Reducer:.forEach(\.path, action: \.path)
  • View:NavigationStack(path: $store.scope(state: \.path, action: \.path))
委托

子功能通过委托 Action 发出结果;父功能在不让子功能了解父功能实现的情况下作出响应。

测试

TestStore 基础
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] }
关键模式
  • 覆盖依赖——测试中绝不调用真实 API
  • 断言所有状态变更——在尾随闭包中进行变更
  • 接收所有 Effect——TestStore 会强制检查完整性
  • TestClock——使用 clock.advance(by:) 控制基于时间的 Effect
  • 集成测试——一起测试组合后的父子功能

高阶 Reducer

用于处理横切关注点(日志记录、分析、指标、功能标志):

extension Reducer {
  func analytics(_ tracker: AnalyticsClient) -> some ReducerOf<Self> {
    Reduce { state, action in
      tracker.track(action)
      return self.reduce(into: &state, action: action)
    }
  }
}

现代 TCA(2025 年及以后)

  • @Reducer 宏会生成样板代码
  • @ObservableState 取代手动使用 WithViewStore
  • @CasePathable 为 Action 启用键路径语法(\.action.child)
  • @Dependency 搭配内置客户端(Clock、UUID、Date)
  • SwiftUI 要求时,在 State 上使用 @MainActor
  • 在视图中直接访问 Store(不再使用 viewStore)

关键规则

应做事项:
  • 保持 Reducer 纯净——仅通过 Effect 执行副作用
  • 使用 IdentifiedArray 管理集合
  • 测试状态转换和 Effect 输出
  • 使用 delegate 在子级与父级之间通信
不应做事项:
  • 在 Reducer 外部修改状态
  • 在 Reducer 中直接调用异步代码
  • 在视图中创建 Store
  • 不要使用 @State/@StateObject 管理 TCA 状态
  • 不要在测试中跳过接收 Action

评论

支持 Markdown