| name | swift-tca |
| description | This skill should be used when the user asks to "create a TCA feature", "add a TCA reducer", "implement TCA navigation", "add a TCA modal", "present a TCA sheet", "use @ObservableState", "add StackState navigation", "implement @Presents", "handle TCA effects", "use Effect.run", "add BindableAction for forms", "create TCA bindings", "use the Delegate pattern", "child-to-parent communication in TCA", or mentions "composable architecture", "pointfree TCA", "TCA state", "TCA action". Covers reducer creation, navigation patterns (Unified Destination, StackState), effect handling, forms with bindings, and parent-child communication. |
| effort | medium |
TCA Feature Development
Patterns for developing features using The Composable Architecture (TCA) with Swift and SwiftUI.
Quick Reference
| Pattern | When to Use | Key Types |
|---|
| Unified Destination | Multiple modals/sheets from one view | @Reducer enum Destination + @Presents |
| StackState | Drill-down navigation (list → detail) | StackState<Path.State> + .forEach |
| Preloading | Jank-free modal presentation | static func preload() async throws -> State |
| Service Layer | Database writes, side effects | @Dependency(\.myService) |
| BindableAction | Forms with two-way binding | BindingReducer() first in body |
| Delegate | Child-to-parent communication | case delegate(Delegate) sub-enum |
| @State Store | Navigation with @Shared children | @State private var store = Store(...) |
Feature Anatomy
Every TCA feature has four components: State, Action, Reducer, and View.
1. State
@Reducer
public struct MyFeature: Sendable {
@ObservableState
public struct State: Equatable {
@ObservationStateIgnored
@Fetch
public var data = MyRequest.Value()
@Shared(.myPreference)
public var preference = false
@Presents
public var destination: Destination.State?
@Presents
public var alert: AlertState<Action.Alert>?
public var isLoading = false
public static func == (lhs: State, rhs: State) -> {
lhs.isLoading rhs.isLoading
lhs.data.rows rhs.data.rows
}
}
}
Key patterns:
@ObservationStateIgnored for expensive properties (@Fetch)
@Shared for cross-app preferences (from swift-sharing)
- Custom
Equatable that excludes @Shared and @Fetch fields
- Single
@Presents var destination for ALL modals (Unified Destination pattern)
2. Actions
public enum Action: Sendable {
case onAppear
case onDisappear
case addButtonTapped
case itemTapped(Item)
case deleteConfirmed(Item.ID)
case loadDataRequest
case loadDataResponse(Result<Void, Error>)
case delegate(Delegate)
public enum Delegate: Equatable, Sendable {
case dismiss
case itemCreated(Item)
}
case destination(PresentationAction<Destination.Action>)
case alert(PresentationAction<Alert>)
public enum Alert: Equatable, Sendable {
case confirmDelete
}
}
Key patterns:
Result<T, Error> for async responses
Delegate sub-enum for parent communication
PresentationAction<T> for modals and alerts
3. Reducer Body
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
return .send(.loadDataRequest)
case .loadDataRequest:
state.isLoading = true
return .run(name: "LoadData") { [data = state.$data] send in
await send(.loadDataResponse(
Result { try await data.load(MyRequest(), animation: .default) }
))
}
case .loadDataResponse(.success):
state.isLoading = false
return .none
case .loadDataResponse(.failure(let error)):
state.isLoading = false
Logger.ui.error("Load failed: \(error)")
return .none
case .delegate:
return .none
case .destination, .alert:
return .none
}
}
.ifLet(\.$destination, action: \.destination)
.ifLet(\.$alert, action: \.alert)
}
Key patterns:
- Shared reducers (
BindingReducer) BEFORE Reduce
- Named effects with
.run(name:) for debugging
.ifLet for optional destinations at END of body
4. View Integration
struct MyView: View {
@Bindable var store: StoreOf<MyFeature>
var body: some View {
List {
ForEach(store.data.rows) { row in
Button { store.send(.itemTapped(row)) } label: {
Text(row.name)
}
}
}
.onAppear { store.send(.onAppear) }
.onDisappear { store.send(.onDisappear) }
.sheet(item: $store.scope(state: \.destination?.addItem, action: \.destination.addItem)) { store in
AddItemView(store: store)
}
.alert($store.scope(state: \.alert, action: \.alert))
}
}
Navigation Patterns
See references/NAVIGATION.md for detailed navigation patterns including:
- Unified Destination enum
- StackState for drill-down
- Preloading for jank-free presentation
- Delegate pattern for dismissal
Quick Navigation Summary
Modals/Sheets (Unified Destination):
@Reducer
public enum Destination: Sendable {
case addItem(AddItemFeature)
case editItem(EditItemFeature)
case settings(SettingsFeature)
}
@Presents public var destination: Destination.State?
.ifLet(\.$destination, action: \.destination)
Drill-down Navigation (StackState):
@Reducer
public enum Path: Sendable {
case detail(DetailFeature)
}
public var path = StackState<Path.State>()
.forEach(\.path, action: \.path)
Effects & Dependencies
CRITICAL: All database writes go through the service layer.
@Dependency(\.itemService) var itemService
case .createItem(let item):
return .run(name: "CreateItem") { [itemService] send in
await send(.createItemResponse(
Result { try await itemService.create(item) }
))
}
Services automatically coordinate:
- Database writes
- Widget reloads
- Notification scheduling
- Logging
Forms with BindableAction
For forms with two-way binding:
public enum Action: BindableAction, Sendable {
case binding(BindingAction<State>)
case saveButtonTapped
}
public var body: some Reducer<State, Action> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
case .saveButtonTapped:
}
}
}
In View:
TextField("Name", text: $store.name)
Toggle("Enabled", isOn: $store.isEnabled)
Code Examples
For complete, copy-paste-ready examples, see examples/EXAMPLES.md.