-
hey there, This screen lives in its own tab as a root view. In another part of the app, I'd like to be able to push this entire This is the struct ContactsView {
var store: StoreOf<ContactsFeature>
...
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
List { ... }
} destination: { store in
ContactDetailsView(store: store)
}
}
} This screen exists on its own as a root view of a tab and works as expected: Now I want to also be able to push this entire screen as a child view from another root view. I naively implemented the same pattern in that other root view and I push in the ContactsView struct RootView: View {
@Bindable
var store: StoreOf<RootFeature>
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
// content of the root view
Button(action: { store.send(.selectContactsButtonTapped) }) {
Text("Select your first contact")
}
Button(action: { store.send(.selectSecondContactsButtonTapped) }) {
Text("Select your second contact")
}
} destination: { store in
switch store.case {
case .selectContacts(let store):
ContactsView(store: store)
case .selectSecondContacts(let store):
ContactsView(store: store)
}
}
}
This does not work because I'm pushing the ContactsView (which is a NavigationStack) into an existing NavigationStack. So the question is how can I have a the
I believe the solution is somewhere in the 04-NavigationStack case study but I'm not sure how. If I remove the NavigationStack from the ContactView, where should I instantiate the child of the ContactView (i.e. destination: { store in
ContactDetailsView store: store)
} ? thanks in advance 🙏 |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
Hi @JanC, as you noted you cannot nest navigation stacks. You really have no choice but to fully flatten the entire navigation hierarchy into the root, which means that it handles both the If you are willing to de-integrate things, and have isolated islands of features, then you can use the type-erased |
Beta Was this translation helpful? Give feedback.
-
I've got it working by using delegate actions and letting the root views handle the navigation. I believe that's what @mbrandonw also suggested in another thread on Slack. Starting from the completed 02-04-navigationstacks, the steps I did to be able to reuse the
This basically extracts the To embedd the
@Reducer
struct ContactsListFeature {
@ObservableState
struct State: Equatable {
var contacts = ContactsFeature.State(contacts: .contacts)
var path = StackState<Path.State>()
}
enum Action {
case contacts(ContactsFeature.Action)
case path(StackActionOf<Path>)
}
@Reducer(state: .equatable)
enum Path {
case contactDetails(ContactDetailsFeature)
}
var body: some ReducerOf<Self> {
Scope(state: \.contacts, action: \.contacts) {
ContactsFeature()
}
Reduce { state, action in
switch action {
case .contacts(.delegate(.didSelectContact(let contact))):
state.path.append(.contactDetails(ContactDetailsFeature.State(contact: contact)))
return .none
case .contacts:
return .none
case .path:
return .none
}
}
.forEach(\.path, action: \.path)
}
}
struct ContactsListView: View {
@Bindable
var store: StoreOf<ContactsListFeature>
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
ContactsView(
store: store.scope(state: \.contacts, action: \.contacts)
)
} destination: { store in
switch store.case {
case .contactDetails(let store):
ContactDetailsView(store: store)
}
}
}
} To push the
@Reducer
struct RootFeature {
@ObservableState
struct State: Equatable {
var path = StackState<Path.State>()
}
enum Action: ViewAction {
enum View {
case selectFirstContact
}
case view(View)
case path(StackActionOf<Path>)
}
@Reducer(state: .equatable)
enum Path {
case contacts(ContactsFeature)
case contactDetails(ContactDetailsFeature)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .view(let viewAction):
return reduceViewAction(&state, viewAction)
case .path(.element(id: _, action: .contacts(.delegate(.didSelectContact(let contact))))):
print("Showing contact details \(contact.firstName)")
state.path.append(.contactDetails(ContactDetailsFeature.State(contact: contact)))
return .none
case .path:
return .none
}
}
.forEach(\.path, action: \.path)
}
private func reduceViewAction(_ state: inout State, _ action: Action.View) -> Effect<Action> {
switch action {
case .selectFirstContact:
state.path.append(.contacts(ContactsFeature.State(contacts: .contacts)))
return .none
}
}
}
@ViewAction(for: RootFeature.self)
struct RootView: View {
@Bindable
var store: StoreOf<RootFeature>
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
List {
Section {
Button(action: { send(.selectFirstContact) }) {
Text("Push contact list")
}
}
}
} destination: { store in
switch store.case {
case .contacts(let store):
ContactsView(store: store)
case .contactDetails(let store):
ContactDetailsView(store: store)
}
}
.navigationTitle("Root")
}
} 🎉 To put those two screens in an actual tab view, it's the same as explained in 01-04-composingfeatures Simulator.Screen.Recording.-.iPhone.15.Pro.M.-.2024-07-31.at.22.20.42.mp4@mbrandonw many thanks for indicating the right direction as well as for providing such a great documentation 🙌 |
Beta Was this translation helpful? Give feedback.
I've got it working by using delegate actions and letting the root views handle the navigation. I believe that's what @mbrandonw also suggested in another thread on Slack.
Starting from the completed 02-04-navigationstacks, the steps I did to be able to reuse the
ContactsView
both as a root of the navigation as well as a child of an existing navigation are the following:NavigationStack
from theContactsView
NavigationLink
by regular buttons that send a view action to the ContactsFeature.StoreContactsFeature.Store
sends a delegation actioncase didSelectContact(Contact)
This basically extracts the
ContactsView
from any navigation.To embedd the
ContactsView
…