Skip to content

Instantly share code, notes, and snippets.

@fededri
Created April 28, 2025 06:23
Show Gist options
  • Save fededri/6b6345ecd96bf4d76c5856185904a7c3 to your computer and use it in GitHub Desktop.
Save fededri/6b6345ecd96bf4d76c5856185904a7c3 to your computer and use it in GitHub Desktop.
TCA Reducer with state in parent domain and enum state
import ComposableArchitecture
import SwiftUI
struct ContentView: View {
let store: StoreOf<TodoReducer>
var body: some View {
VStack {
Button("Change state to error") {
store.send(.setLoadedState(.failure(FeatureError.unknown)))
}
Button("Change state to loading") {
store.send(.setContentState(.loading))
}
Button("Change state to loaded") {
store.send(.onAppear)
}
Text(store.someParentFeatureState)
Group {
switch store.contentState {
case .loading:
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .loaded:
if let loadedStore = store.scope(
state: \.contentState.loaded,
action: \.loaded
) {
LoadedView(store: loadedStore)
}
case .error:
Text("An error occurred.")
.foregroundColor(.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear(perform: {
store.send(.onAppear)
})
.padding()
}
}
#Preview {
ContentView(store: Store(initialState: TodoReducer.State(), reducer: {
TodoReducer()
}))
}
@Reducer
struct TodoReducer: Reducer {
@Dependency(\.continuousClock) var clock
@ObservableState
struct State: Equatable {
var someParentFeatureState: String = ""
var contentState: ContentState = .loading
@CasePathable
@dynamicMemberLookup // Key, without this code does not compile.
enum ContentState: Equatable {
case loading
case loaded(Loaded.State)
case error(ErrorReducer.State)
}
}
enum Action {
case onAppear
case loaded(Loaded.Action)
case error(ErrorReducer.Action)
case setLoadedState(Result<Loaded.State, Error>)
case setContentState(State.ContentState)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
state.contentState = .loading
return .run { send in
await send(
.setLoadedState(
Result(catching: {
try await clock.sleep(for: .seconds(1)) // simulate some operation
// Initialize Loaded.State with dummy data here
return Loaded.State(list: ["Item A", "Item B", "Item C"])
})
))
}
case .loaded(.userDidTapItem(let item)):
print("item tapped: \(item)")
return .none
case .loaded:
return .none
case .error:
return .none
case .setLoadedState(.success(let loadedState)):
state.contentState = .loaded(loadedState)
return .none
case .setLoadedState(.failure):
state.contentState = .error(.init())
return .none
case .setContentState(let contentState):
state.contentState = contentState
return .none
}
}
.ifLet(\.contentState.loaded, action: \.loaded) {
Loaded()
}
.ifLet(\.contentState.error, action: \.error) {
ErrorReducer()
}
}
}
@Reducer
struct Loaded: Reducer {
@ObservableState
struct State: Equatable {
var list: [String] = []
}
enum Action {
case userDidTapItem(String)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .userDidTapItem(let item):
print("received action in LoadedReducer: \(item)")
return .none
}
}
}
}
struct LoadedView: View {
let store: StoreOf<Loaded>
var body: some View {
List(store.list, id: \.self) { item in
Button(item) {
store.send(.userDidTapItem(item))
}
}
}
}
@Reducer
struct ErrorReducer: Reducer {
@ObservableState
struct State: Equatable {
var errorMessage: String = ""
}
enum Action {
}
var body: some ReducerOf<Self> {
EmptyReducer()
}
}
enum FeatureError: Error {
case unknown
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment