Is that the case? I've never heard that before 🤔Do you have a link where this is stated/explained? That would make me rethink some of the architectural decisions in my app. I assumed the entire purpose of that modifier is to place it anywhere in a NavigationStack, even on sub-pages.
I often encountered similar statements when searching for solutions to problems with NavigationStack. For example, here is the first link that I found now with the answer from Apple Engineer: https://forums.developer.apple.com/forums/thread/727307?answerId=749141022#749141022
Quote from the answer:
move navigationDestination modifiers as high up in the view hierarchy. This is more efficient for the Navigation system to read up front than with potentially every view update.
The problem with moving everything to ContentView is that you lose all modularity. ContentView would be tightly coupled with MiddleView, which is what I really want to avoid.
After workarounding a number of problems with NavigationStack, I came to the following solution for myself, maybe it may be useful:
do not use NavigationPath, only an array (depending on the OS version, there were different problems).
place navigationDestination on the root view of NavigationStack.
use a synchronized @State variable for the path (yes, as you said, would not like to use this, but the absence of unnecessary re-initializations / body calls calms me down)
To solve the problem with modularization, you can use ViewBuilder or View for different parts of the path.
Simplified example:
import SwiftUI
enum Destination: Hashable {
case flow1(Flow1Destination)
case flow2(Flow2Destination)
}
struct ContentView: View {
@State var path: [Destination] = []
var body: some View {
NavigationStack(path: $path) {
RootView(path: $path)
.navigationDestination(for: Destination.self) { destination in
switch destination {
case let .flow1(destination):
Flow1FactoryView(destination: destination, path: $path, getDestination: { .flow1($0) })
case let .flow2(destination):
Flow2FactoryView(destination: destination, path: $path, getDestination: { .flow2($0) })
}
}
}
}
}
struct RootView: View {
@Binding var path: [Destination]
var body: some View {
VStack {
Button("Flow1") {
path.append(.flow1(.details))
}
Button("Flow2") {
path.append(.flow2(.login))
}
}
.navigationTitle("Root")
}
}
enum Flow1Destination: Hashable {
case details
case more
}
struct Flow1FactoryView<Destination: Hashable>: View {
let destination: Flow1Destination
@Binding var path: [Destination]
let getDestination: (Flow1Destination) -> Destination
var body: some View {
switch destination {
case .details: DetailsView(onShowMore: { path.append(getDestination(.more)) })
case .more: MoreView()
}
}
}
struct DetailsView: View {
let onShowMore: () -> Void
var body: some View {
Button("Show more", action: onShowMore)
}
}
struct MoreView: View {
var body: some View {
Text("No more")
}
}
enum Flow2Destination: Hashable {
case login
case forgot
}
struct Flow2FactoryView<Destination: Hashable>: View {
let destination: Flow2Destination
@Binding var path: [Destination]
let getDestination: (Flow2Destination) -> Destination
var body: some View {
switch destination {
case .login: LoginView(onForgotPassword: { path.append(getDestination(.forgot)) })
case .forgot: ForgotView(onClose: { path.removeLast() })
}
}
}
struct LoginView: View {
let onForgotPassword: () -> Void
var body: some View {
VStack {
Text("Login")
Button("Forgot?", action: onForgotPassword)
}
}
}
struct ForgotView: View {
let onClose: () -> Void
var body: some View {
Button("Forgot", action: onClose)
}
}
#Preview {
ContentView()
}