TCA Feature Development
Workflow
- Explore first — read the target module and its neighbours before writing anything
- Create the reducer — see
references/feature-template.md
- Create the view — see
references/view-patterns.md
- Add previews — one per meaningful state; see Preview Rules below
- Write tests — see
references/testing-patterns.md
- Register — add target to
Package.swift; wire into parent if it's a new tab
Explore Before Writing
Before implementing any feature, read:
- An existing feature's reducer + view (understand current patterns)
- The parent module (understand how this feature is composed in)
Sources/Services/ for available dependency clients
Sources/Models/ for the relevant data types
Reducer Checklist
@Reducer macro on a struct
@ObservableState on State, which conforms to Equatable
public init() {} when all state fields have defaults; explicit memberwise init otherwise
- Idempotency guard in
.onAppear: guard state.data == nil else { return .none }
@Reducer enum Destination when the feature navigates to children (@Presents + ifLet)
delegate(Delegate) action + @CasePathable enum Delegate for upward communication
case .delegate: return .none — the reducer always ignores its own delegate cases
- Capture dependencies explicitly in
.run: [client = someClient]
- Errors stored as
String? via error.localizedDescription, not as Error type
View Checklist
@Bindable var store: StoreOf<FeatureReducer> — always @Bindable
public init(store:) — always explicit public init
store.send(.onAppear) in .onAppear modifier
- Use
Group { } for multi-branch body (loading / error / empty / content)
- Use
ContentUnavailableView for error and empty states
- Wire navigation destinations with
$store.scope(state:action:)
- Toggle bindings:
Binding(get: { store.flag }, set: { _ in store.send(.toggleFlag) })
- Break complex body into
@ViewBuilder private var computed properties
Preview Rules
Every view must have at minimum 4 previews: loading (live reducer, fires .onAppear), content (pre-built state with sample data), empty, and any feature-specific variants (error, completed, failed, etc.).
- Wrap in
NavigationStack { } when the view uses .navigationTitle or .navigationDestination
- Name format:
"FeatureName - StateName" e.g. "Quiz - Partially Answered"
- Do not use
withDependencies in previews — use the live reducer directly
- Add
static var sample: Self / static var samples: [Self] on model types for preview data
See references/feature-template.md for complete #Preview block templates (Patterns A–D).
Key Decisions
| Question |
Answer |
| Reducer + View in one file? |
Yes, when combined ≤ ~200 lines |
| Where do computed properties go? |
On State, not the view |
| How to pass data to a child feature? |
Initialize child's State when setting destination |
| How to communicate upward? |
.delegate(Delegate) action |
| Where does error text live? |
state.loadError: String? via error.localizedDescription |
| How to cancel in-flight effects? |
.cancellable(id:, cancelInFlight: true) |
| Where do sub-view components live? |
File-private structs in the same .swift file |
Should I use UIViewRepresentable? |
Only when SwiftUI has no equivalent (e.g. MKMapView, WKWebView). Never for styling convenience. |
| Which concurrency primitive? |
async/await + AsyncStream first; Combine only if the API has no async alternative; GCD/NSLock only for legacy C/ObjC callbacks |
Reference Files
references/feature-template.md — Complete file templates: reducer, view with previews, tests, Package.swift snippet, AppCore wiring. Read when starting a new feature from scratch.
references/view-patterns.md — View sub-patterns: loading/error/empty states, navigation wiring, Toggle binding, private sub-views, file-local components, animation. Read when implementing or refining a view.
references/testing-patterns.md — TestStore setup, dependency mocking, async flow testing, delegate testing, computed property tests, idempotency tests. Read when writing or debugging tests.
1---2name: tca-developer3description: Develop new features in a modular iOS app using The Composable Architecture (TCA) and Swift Package Manager. Use when implementing a new screen, adding a sub-feature, extending an existing reducer, adding SwiftUI previews, or writing TCA tests. Covers the full development workflow: reading the existing codebase, creating reducer + view files, wiring navigation, writing SwiftUI previews for all meaningful states (loaded, empty, error, and feature-specific variants), and writing TestStore tests. Always explore the target module before writing code to match its existing style and conventions.4---56# TCA Feature Development78## Workflow9101. **Explore first** — read the target module and its neighbours before writing anything112. **Create the reducer** — see `references/feature-template.md`123. **Create the view** — see `references/view-patterns.md`134. **Add previews** — one per meaningful state; see Preview Rules below145. **Write tests** — see `references/testing-patterns.md`156. **Register** — add target to `Package.swift`; wire into parent if it's a new tab1617## Explore Before Writing1819Before implementing any feature, read:20- An existing feature's reducer + view (understand current patterns)21- The parent module (understand how this feature is composed in)22- `Sources/Services/` for available dependency clients23- `Sources/Models/` for the relevant data types2425## Reducer Checklist2627- `@Reducer` macro on a `struct`28- `@ObservableState` on `State`, which conforms to `Equatable`29- `public init() {}` when all state fields have defaults; explicit memberwise init otherwise30- Idempotency guard in `.onAppear`: `guard state.data == nil else { return .none }`31- `@Reducer enum Destination` when the feature navigates to children (`@Presents` + `ifLet`)32- `delegate(Delegate)` action + `@CasePathable enum Delegate` for upward communication33- `case .delegate: return .none` — the reducer always ignores its own delegate cases34- Capture dependencies explicitly in `.run`: `[client = someClient]`35- Errors stored as `String?` via `error.localizedDescription`, not as `Error` type3637## View Checklist3839- `@Bindable var store: StoreOf<FeatureReducer>` — always `@Bindable`40- `public init(store:)` — always explicit public init41- `store.send(.onAppear)` in `.onAppear` modifier42- Use `Group { }` for multi-branch body (loading / error / empty / content)43- Use `ContentUnavailableView` for error and empty states44- Wire navigation destinations with `$store.scope(state:action:)`45- Toggle bindings: `Binding(get: { store.flag }, set: { _ in store.send(.toggleFlag) })`46- Break complex body into `@ViewBuilder private var` computed properties4748## Preview Rules4950Every view **must** have at minimum 4 previews: **loading** (live reducer, fires `.onAppear`), **content** (pre-built state with sample data), **empty**, and any **feature-specific variants** (error, completed, failed, etc.).5152- Wrap in `NavigationStack { }` when the view uses `.navigationTitle` or `.navigationDestination`53- Name format: `"FeatureName - StateName"` e.g. `"Quiz - Partially Answered"`54- Do **not** use `withDependencies` in previews — use the live reducer directly55- Add `static var sample: Self` / `static var samples: [Self]` on model types for preview data5657See `references/feature-template.md` for complete `#Preview` block templates (Patterns A–D).5859## Key Decisions6061| Question | Answer |62|----------|--------|63| Reducer + View in one file? | Yes, when combined ≤ ~200 lines |64| Where do computed properties go? | On `State`, not the view |65| How to pass data to a child feature? | Initialize child's `State` when setting `destination` |66| How to communicate upward? | `.delegate(Delegate)` action |67| Where does error text live? | `state.loadError: String?` via `error.localizedDescription` |68| How to cancel in-flight effects? | `.cancellable(id:, cancelInFlight: true)` |69| Where do sub-view components live? | File-private structs in the same `.swift` file |70| Should I use `UIViewRepresentable`? | Only when SwiftUI has no equivalent (e.g. `MKMapView`, `WKWebView`). Never for styling convenience. |71| Which concurrency primitive? | `async/await` + `AsyncStream` first; Combine only if the API has no async alternative; GCD/NSLock only for legacy C/ObjC callbacks |7273## Reference Files7475- **`references/feature-template.md`** — Complete file templates: reducer, view with previews, tests, Package.swift snippet, AppCore wiring. Read when starting a new feature from scratch.76- **`references/view-patterns.md`** — View sub-patterns: loading/error/empty states, navigation wiring, Toggle binding, private sub-views, file-local components, animation. Read when implementing or refining a view.77- **`references/testing-patterns.md`** — TestStore setup, dependency mocking, async flow testing, delegate testing, computed property tests, idempotency tests. Read when writing or debugging tests.