swift-composable-architecture
You are an expert in The Composable Architecture (TCA) by Point-Free. Help developers write correct, testable, and composable Swift code following TCA patterns.
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
Effectvalues - Composition over inheritance: Small, isolated, recombinable modules
- Testability first: Every feature testable with
TestStore
The Four Building Blocks
- State – Data for UI and logic (
@ObservableState struct) - Action – All events: user actions, effects, delegates (
enumwith@CasePathable) - Reducer – Pure function evolving state, returning effects (
@Reducer macro) - Store – Runtime connecting state, reducer, and views (
StoreOf<Feature>)
Feature Structure
import SwiftTCACore
public extension SwiftTCACore {
enum Feature {}
}
// MARK: - 视图
extension SwiftTCACore.Feature {
struct FeatureView: View {
let store: StoreOf<Feature>
var body: some View {
List(store.items) { item in
Text(item.title)
}
.onAppear { store.send(.onAppear) }
}
}
}
extension SwiftTCACore.Feature.FeatureView {
@Reducer public struct Store {
// MARK: - 状态
@ObservableState struct State: Equatable {
var items: IdentifiedArrayOf<Item> = []
var isLoading = false
}
// MARK: - 事件
@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(core)
}
func core(into state: inout State, action: Action) -> Effect<Action> {
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
}
}
}
}
typealias FeatureView = SwiftTCACore.Feature.FeatureView
extension FeatureView.Store.State: @unchecked @retroactive Sendable {
static let `default`: SwiftTCACore.Feature.FeatureView.Store.State = .init()
}
Effects
| Pattern | Use 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 |
Cancellation
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.
Dependencies
Built-in Dependencies
@Dependency(\.uuid), @Dependency(\.date), @Dependency(\.continuousClock), @Dependency(\.mainQueue)
Custom Dependencies
- Define client struct with closures
- Conform to
DependencyKeywithliveValue,testValue,previewValue - Extend
DependencyValueswith computed property - Use
@Dependency(\.yourClient)in reducer
Test override: withDependencies { $0.apiClient.fetch = { .mock } }
Composition
Child Features
Use Scope to embed children:
var body: some ReducerOf<Self> {
Scope(state: \.child, action: \.child) { ChildFeature() }
Reduce { state, action in ... }
}
View: ChildView(store: store.scope(state: \.child, action: \.child))
Collections
Use IdentifiedArrayOf<ChildFeature.State> with .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))
Stack-Based (NavigationStack, deep linking)
- Model with
StackState<Path.State>andStackActionOf<Path> - Define
@Reducer enum Path { case detail(DetailFeature) ... } - Reducer:
.forEach(\.path, action: \.path) - View:
NavigationStack(path: $store.scope(state: \.path, action: \.path))
Delegates
Child emits delegate actions for outcomes; parent responds without child knowing parent's implementation.
Testing
TestStore Basics
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
Higher-Order Reducers
For cross-cutting concerns (logging, analytics, metrics, feature flags):
extension Reducer {
func analytics(_ tracker: AnalyticsClient) -> some ReducerOf<Self> {
Reduce { state, action in
tracker.track(action)
return self.reduce(into: &state, action: action)
}
}
}
Modern TCA (2025+)
@Reducermacro generates boilerplate@ObservableStatereplaces manualWithViewStore@CasePathableenables key path syntax for actions (\.action.child)@Dependencywith built-in clients (Clock, UUID, Date)@MainActoron State when SwiftUI requires it- Direct store access in views (no more
viewStore)
Critical Rules
DO:
- Keep reducers pure - side effects through
Effectonly - Use
IdentifiedArrayfor 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/@StateObjectfor TCA-managed state - Skip receiving actions in tests
More from hocgin/agent-skills
swift-private-bundle
Use when working with private Swift Package Manager dependencies from github.com/hocgin, especially when you need to discover, verify, or integrate a package and should first refresh and search the local ~/GitHub/knowledge mirror of github.com/hocgin/knowledge, then fall back to gh and the target repository when needed.
6swift-sqlite-data
Use when working with SQLiteData library (@Table, @FetchAll, @FetchOne macros) for SQLite persistence, queries, writes, migrations, or CloudKit private database sync.
6article-writer
AI驱动的智能写作系统,专注于创作高质量、低AI检测率的文章内容
5swift-localization
Best practices for internationalizing Swift/SwiftUI applications using LocalizedStringResource, String Catalogs (.xcstrings), and type-safe localization patterns. Use when implementing multi-language support, adding new UI strings, or refactoring hardcoded text in Swift apps.
5wrangler
Cloudflare Workers CLI for deploying, developing, and managing Workers, KV, R2, D1, Vectorize, Hyperdrive, Workers AI, Containers, Queues, Workflows, Pipelines, and Secrets Store. Load before running wrangler commands to ensure correct syntax and best practices.
4xlsx
Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.
4