Loading...
Loading...
Use when building, refactoring, debugging, or testing iOS/macOS features using The Composable Architecture (TCA). Covers feature structure, effects, dependencies, navigation patterns, and testing with TestStore.
npx skill4agent add tryhuset/agent-skills swift-composable-architectureEffectTestStore@ObservableState structenum@CasePathable@Reducer macroStoreOf<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
}
}
}
}struct FeatureView: View {
let store: StoreOf<Feature>
var body: some View {
List(store.items) { item in
Text(item.title)
}
.onAppear { store.send(.onAppear) }
}
}| Pattern | Use Case |
|---|---|
| Synchronous state change, no side effect |
| Async work, send actions back |
| Long-running/replaceable effects |
| Cancel a running effect |
| Run multiple effects in parallel |
| Run effects sequentially |
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@Dependency(\.uuid)@Dependency(\.date)@Dependency(\.continuousClock)@Dependency(\.mainQueue)DependencyKeyliveValuetestValuepreviewValueDependencyValues@Dependency(\.yourClient)withDependencies { $0.apiClient.fetch = { .mock } }Scopevar body: some ReducerOf<Self> {
Scope(state: \.child, action: \.child) { ChildFeature() }
Reduce { state, action in ... }
}ChildView(store: store.scope(state: \.child, action: \.child))IdentifiedArrayOf<ChildFeature.State>.forEach(\.items, action: \.items) { ChildFeature() }@Presents var detail: DetailFeature.State?case detail(PresentationAction<DetailFeature.Action>).ifLet(\.$detail, action: \.detail) { DetailFeature() }.sheet(item: $store.scope(state: \.detail, action: \.detail))StackState<Path.State>StackActionOf<Path>@Reducer enum Path { case detail(DetailFeature) ... }.forEach(\.path, action: \.path)NavigationStack(path: $store.scope(state: \.path, action: \.path))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] }clock.advance(by:)extension Reducer {
func analytics(_ tracker: AnalyticsClient) -> some ReducerOf<Self> {
Reduce { state, action in
tracker.track(action)
return self.reduce(into: &state, action: action)
}
}
}@Reducer@ObservableStateWithViewStore@CasePathable\.action.child@Dependency@MainActorviewStoreEffectIdentifiedArray@State@StateObject