关于 swift-composable-architecture
swift-composable-architecture 是帮助 Swift 开发者使用 Point-Free 的 The Composable Architecture(TCA)构建可测试应用的技能。重点包括 Reducer、显式副作用、依赖管理、功能组合、导航和基于 TestStore 的测试。
主要功能
- 01以 Reducer 驱动状态与动作流
- 02显式异步副作用与取消
- 03依赖注入与测试替换
- 04子功能组合与导航
- 05TestStore 和 TestClock 测试模式
适用场景
- 01使用 TCA 构建 SwiftUI 功能模块
- 02通过依赖隔离异步工作
- 03测试状态转换和定时副作用
- 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测试
四个构建模块
- State – UI 和逻辑的数据(
@ObservableState struct) - Action – 所有事件:用户操作、Effect、委托(使用
@CasePathable的enum) - Reducer – 演进 State 并返回 Effect 的纯函数(
@Reducer macro) - 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)
自定义依赖
- 定义包含闭包的客户端结构体
- 遵循
DependencyKey,并提供liveValue、testValue、previewValue - 为
DependencyValues添加计算属性 - 在 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