-
I am failing to understand what is needed to have mutations on a detail view synchronised with the parent view that is triggering the drill down with stack navigation. I do know how to do this with tree navigation, but in the app I would need this, we need to have a stack navigation for this. In this most simple sample I have both features integrated and drilling down works, but it seems as though I am missing the part where I would need to sync back to the parent. Here's the code: import ComposableArchitecture
import SwiftUI
@main
struct so_tca_StackNavigationUpdateParentFromChildApp: App {
var body: some Scene {
WindowGroup {
ParentView(
store: Store(
initialState: ParentFeature.State(),
reducer: { ParentFeature() })
)
}
}
}
struct Item: Equatable, Identifiable {
var id: UUID
var text: String
init(
id: UUID = UUID(),
text: String = "New item"
) {
self.id = id
self.text = text
}
}
@Reducer
struct ParentFeature {
@ObservableState
struct State {
var items: [Item]
var path = StackState<Path.State>()
init(
items: [Item] = [
Item(text: "Item A"),
Item(text: "Item B"),
Item(text: "Item C")
]
) {
self.items = items
}
}
enum Action {
case path(StackAction<Path.State, Path.Action>)
}
@Reducer
struct Path {
@ObservableState
enum State {
case itemDetail(ChildFeature.State)
}
enum Action {
case itemDetail(ChildFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: /State.itemDetail, action: /Action.itemDetail) {
ChildFeature()
}
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .path:
return .none
}
}
.forEach(\.path, action: /Action.path) {
Path()
}
}
}
struct ParentView: View {
@State var store: StoreOf<ParentFeature>
var body: some View {
NavigationStack(
path: $store.scope(state: \.path, action: \.path)
) {
List {
Section {
ForEach(store.items) { item in
NavigationLink(state: ParentFeature.Path.State.itemDetail(ChildFeature.State(item: item))) {
Text(item.text)
}
}
} footer: {
Text("Changes in detail do *not* update values here …")
}
}
} destination: { store in
switch store.state {
case .itemDetail:
if let store = store.scope(
state: \.itemDetail,
action: \.itemDetail
) {
ChildView(store: store)
}
}
}
}
}
@Reducer
struct ChildFeature {
@ObservableState
struct State {
var item: Item
var mutatingItem: Item
var hasNoChanges: Bool {
item == mutatingItem
}
init(
item: Item
) {
self.item = item
self.mutatingItem = item
}
}
enum Action: BindableAction {
case binding(BindingAction<State>)
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
}
}
}
}
struct ChildView: View {
@State var store: StoreOf<ChildFeature>
var body: some View {
Form {
Section {
TextField("Item", text: $store.mutatingItem.text)
} footer: {
Text("How can I synchronize changes here with the parent?")
}
}
}
}
#Preview {
ParentView(
store: Store(
initialState: ParentFeature.State(),
reducer: { ParentFeature() })
)
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Turns out that I had not understood the reasoning behind case .path(.popFrom(id: let id)):
guard let childState = state.path[id: id]?.itemDetail
else { return .none }
let mutatingItem = childState.mutatingItem
if let index = state.items.firstIndex(where: { $0.id == mutatingItem.id }) {
state.items[index] = mutatingItem
}
return .none For completeness sake, here's the entire code: import ComposableArchitecture
import SwiftUI
@main
struct so_tca_StackNavigationUpdateParentFromChildApp: App {
var body: some Scene {
WindowGroup {
ParentView(
store: Store(
initialState: ParentFeature.State(),
reducer: { ParentFeature() })
)
}
}
}
struct Item: Equatable, Identifiable {
var id: UUID
var text: String
init(
id: UUID = UUID(),
text: String = "New item"
) {
self.id = id
self.text = text
}
}
@Reducer
struct ParentFeature {
@ObservableState
struct State {
var items: [Item]
var path = StackState<Path.State>()
init(
items: [Item] = [
Item(text: "Item A"),
Item(text: "Item B"),
Item(text: "Item C")
]
) {
self.items = items
}
}
enum Action {
case path(StackAction<Path.State, Path.Action>)
}
@Reducer
struct Path {
@ObservableState
enum State {
case itemDetail(ChildFeature.State)
}
enum Action {
case itemDetail(ChildFeature.Action)
}
var body: some ReducerOf<Self> {
Scope(state: /State.itemDetail, action: /Action.itemDetail) {
ChildFeature()
}
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .path(.popFrom(id: let id)):
guard let childState = state.path[id: id]?.itemDetail
else { return .none }
let mutatingItem = childState.mutatingItem
if let index = state.items.firstIndex(where: { $0.id == mutatingItem.id }) {
state.items[index] = mutatingItem
}
return .none
case .path:
return .none
}
}
.forEach(\.path, action: /Action.path) {
Path()
}
}
}
struct ParentView: View {
@State var store: StoreOf<ParentFeature>
var body: some View {
NavigationStack(
path: $store.scope(state: \.path, action: \.path)
) {
List {
Section {
ForEach(store.items) { item in
NavigationLink(state: ParentFeature.Path.State.itemDetail(ChildFeature.State(item: item))) {
Text(item.text)
}
}
} footer: {
Text("Changes in detail do *not* update values here …")
}
}
} destination: { store in
switch store.state {
case .itemDetail:
if let store = store.scope(
state: \.itemDetail,
action: \.itemDetail
) {
ChildView(store: store)
}
}
}
}
}
@Reducer
struct ChildFeature {
@ObservableState
struct State {
var item: Item
var mutatingItem: Item
var hasNoChanges: Bool {
item == mutatingItem
}
init(
item: Item
) {
self.item = item
self.mutatingItem = item
}
}
enum Action: BindableAction {
case binding(BindingAction<State>)
}
var body: some ReducerOf<Self> {
BindingReducer()
Reduce { state, action in
switch action {
case .binding:
return .none
}
}
}
}
struct ChildView: View {
@State var store: StoreOf<ChildFeature>
var body: some View {
Form {
Section {
TextField("Item", text: $store.mutatingItem.text)
} footer: {
Text("How can I synchronize changes here with the parent?")
}
}
}
}
#Preview {
ParentView(
store: Store(
initialState: ParentFeature.State(),
reducer: { ParentFeature() })
)
} |
Beta Was this translation helpful? Give feedback.
Turns out that I had not understood the reasoning behind
popFrom(id:)
at all...The following solution was brought to me in the project's Slack by Marius K. In the parent's reducer, I can listen to
popFrom(id:)
and sync changes back on dismissal of the child like this:For completeness sake, here's the entire code: