-
Very new to TCA, I cannot figure out why a view reducer is properly getting actions when isolated (like when its store is provided in a preview), but not anymore when the store is derived from a parent store. In that case, the child stops receiving actions and they are all caught by the parent. Here is my simple child view, meant to be in a sheet: struct AddSourceReducer: ReducerProtocol {
struct State: Equatable {
var name: String = ""
var canBeAdded: Bool = false
}
enum Action: Equatable {
case nameChanged(String)
case cancelTapped
case addTapped
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case let .nameChanged(newName):
state.name = newName
state.canBeAdded = state.name.count >= 3
return .none
case .cancelTapped:
return .none
case .addTapped:
return .none
}
}
}
struct AddSourceView: View {
var store: StoreOf<AddSourceReducer>
var body: some View {
WithViewStore(self.store, observe: { $0 } ) { viewStore in
NavigationView {
Form {
TextField("Source Name", text: viewStore.binding(get: \.name, send: { .nameChanged($0) }))
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { viewStore.send(.cancelTapped) }
}
ToolbarItem(placement: .confirmationAction) {
Button("Save", action: { viewStore.send(.addTapped) })
.disabled(!viewStore.canBeAdded)
}
}
}
}
}
} It works fine if I put it as the sole component of my app like this: AddSourceView(store: Store(initialState: AddSourceReducer.State(), reducer: AddSourceReducer())) Now when displayed in a sheet like this, it stops working (the struct SourcesReducer: ReducerProtocol {
struct State: Equatable {
var sources: [String]
var addSourceVisible: Bool = false
var sourceToAdd = AddSourceReducer.State()
}
enum Action: Equatable {
case addSourceTapped
case addSourceDismissed
case addSource(AddSourceReducer.Action)
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .addSourceTapped:
state.addSourceVisible = true
return .none
case .addSourceDismissed:
state.addSourceVisible = false
return .none
case .addSource(.addTapped):
print("Add", state.sourceToAdd.name)
state.sources.append(state.sourceToAdd.name)
state.addSourceVisible = false
state.sourceToAdd.name = ""
state.sourceToAdd.canBeAdded = false
return .none
case let .addSource(childAction):
// Why is the .nameChanged action getting here and not in the AddSource reducer?
print("Child Action", childAction)
return .none
}
}
}
struct SourcesView: View {
let store: StoreOf<SourcesReducer>
struct ViewState: Equatable {
var sources: [String]
var addSourceVisible: Bool
init(state: SourcesReducer.State) {
self.sources = state.sources
self.addSourceVisible = state.addSourceVisible
}
}
var body: some View {
WithViewStore(self.store, observe: ViewState.init ) { viewStore in
NavigationView {
Form {
List {
ForEach(viewStore.sources, id: \.self) { source in
Text(source)
}
}
Button(action: { viewStore.send(.addSourceTapped) }) {
Label("Add a New Source", systemImage: "plus.circle")
}
}
.navigationTitle("Sources")
.sheet(isPresented: viewStore.binding(get: \.addSourceVisible, send: .addSourceDismissed)) {
AddSourceView(store: self.store.scope(
state: \.sourceToAdd,
action: SourcesReducer.Action.addSource
))
}
}
}
}
} There might be better ways to code this, but I would first like to understand why the Actions get passed to the parent, that needs to receive Notes:
|
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
Hi @iMarti, what you have is close. While you are correctly transforming stores by using the You can do this using the struct SourcesReducer: ReducerProtocol {
…
var body: some ReducerProtocol<State, Action> {
Scope(state: \.sourceToAdd, action: /Action.addSource) {
AddSourceReducer()
}
Reduce { state, action in
switch action {
case .addSourceTapped:
…
case .addSourceDismissed:
…
case .addSource(.addTapped):
…
case let .addSource(childAction):
…
}
}
}
} This makes it so that |
Beta Was this translation helpful? Give feedback.
Hi @iMarti, what you have is close. While you are correctly transforming stores by using the
.scope
operator, the key concept you are missing is reducer composition. You have a child reducer (AddSourceReducer
) and a parent reducer (SourcesReducer
) that contains the child reducer, and you need to express that fact by composing the reducers together.You can do this using the
Scope
operator along with reducer builders. In yourSourcesReducer
you can implement its logic with abody
property, which allows you to compose multiple reducers together: