Multiple NavigationStack In NavigationSplitView Issue

I have an app using an NavigationSplitView with a list on the left and detail views on the right. When I select an item from the list on the left, the detail view on the should change and each detail view can push other views. I use a Navigation Stack in each root detail view. The NavigationStack does not seem to be working as expected. 

I created a simple example app to show what I am seeing. In the app:

  • Select Atlantic from the list on the left
  • Tap on Atlantic View in the detail view to push to Atlantic Pushed View
  • Select Pacific from the list on the left

The view on the right still shows Atlantic Pushed View when I would expect it to show Pacific View.  If on the detail view I tap the back arrow, it goes from Atlantic Pushed View to Pacific View which is not right.

It seems like when I selected Pacific from the left, it replace the root Atlantic view with the root Pacific view in the NavigationStack which is not correct. It also seems like no matter what I try, there is only one NavigationStack path used throughout the app.

The behavior I want and would expect is:

  • Select Atlantic from the list on the left
  • Tap on Atlantic View in the detail view to push to Atlantic Pushed View
  • Select Pacific from the list on the left, which which should show Pacific View
  • Select Atlantic from the list on the left, which should  go back to Pushed Atlantic View

I am considering filing a big report but I wanted to see if  I am missing something or is this just not working. My sample code it below.

import SwiftUI

struct Ocean: Identifiable, Hashable {
	let id: String
}

private var oceans = [
	Ocean(id: "Atlantic"),
	Ocean(id: "Pacific"),
]

struct ContentView: View {
	@State var selection: Set<String> = [oceans[0].id]
	
	var body: some View {
		NavigationSplitView {
			List(oceans, selection: $selection) { ocean in
				Text("\(ocean.id) Ocean")
			}
			.navigationTitle("Oceans")
		} detail: {
			if selection.first == "Atlantic" {
				AtlanticView(selection: selection)
			}
			else if selection.first == "Pacific" {
				PacificView(selection: selection)
			}
			else {
				Text("Unknown Ocean")
			}
		}
	}
}

struct AtlanticView: View {
	@State var selection: Set<String>
	@State private var atlanticPath = NavigationPath()
	var body: some View {
		NavigationStack(path: $atlanticPath ) {
			VStack {
				NavigationLink {
					AtlanticPushedView(selection: selection)
				} label: {
					Text("Atlantic View\n\(selection.first!) Ocean")
				}
			}
		}
	}
}

struct AtlanticPushedView: View {
	var selection: Set<String>
	var body: some View {
		Text("Pushed View\n\(selection.first!) Ocean")
	}
}

struct PacificView: View {
	var selection: Set<String>
	@State private var pacificPath = NavigationPath()
	var body: some View {
		NavigationStack(path: $pacificPath ) {
			NavigationLink {
				PacificPushedView(selection: selection)
			} label: {
				Text("Pacific View\n\(selection.first!) Ocean")
			}
		}
	}
}

struct PacificPushedView: View {
	var selection: Set<String>
	var body: some View {
		Text("Pacific Pushed View\n\(selection.first!) Ocean")
	}
}

Replies

I have an example in an unrelated reply (https://developer.apple.com/forums/thread/716804?answerId=731260022#731260022) that does much of what you are trying to do? If I have time I'll try to rework your example code but in the mean time I hope this helps:

Some notes: I'm not sure why your selection is a set of one instead of an Ocean? That is the biggest ?? to me. Are you trying to pass a deep link? In which case why aren't you setting the path with it? If you're trying to manage deep links I'd recommend making a NavigationManager / Coordinator / Store class that lives all the way at the top.

(Ocean will need to be Hashable to use it as an .id() parameter)


struct SplitViewLanding: View {
    var options = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]

    //NOTE: the selection binding is an an optional. 
    @State var selection:String?

    var body: some View {
        NavigationSplitView {
            Button("Nil Selection") { selection = nil }
            List(options, id:\.self, selection: $selection) { o in
                Text("\(o)")
            }
        } detail: {
            if let selection {
                RootForDetailView(for: selection).id(selection)
            }

        }
    }
}
class DetailDoNothingVM:ObservableObject {
    @Published var optionSet:String

    deinit {
        print("DetailDoNothingVM DEINIT")
    }

    init() {
        print("DetailDoNothingVM INIT")
        self.optionSet = "Default"
    }

}
struct RootForDetailView: View {
    @StateObject var viewModel = DetailDoNothingVM()
    let optionSet:String

    init(for optionSet:String) {
        self.optionSet = optionSet
    }

    @State var path = NavigationPath()

    let int = Int.random(in: 0...100)

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Text("Hello \(int)")
                Button("Go Forward") {
                    path.append(Int.random(in: 0...100))
                }
            }.navigationDestination(for: Int.self) { int in
                DetailOptionIntView(int: int, path: $path).environmentObject(viewModel)
            }.navigationTitle("\(viewModel.optionSet): \(int)")
        }.onAppear() {
            viewModel.optionSet = optionSet
        }
    }
}
struct DetailOptionIntView:View {
    let int:Int
    @Binding var path:NavigationPath
    @EnvironmentObject var viewModel:DetailDoNothingVM

    var body: some View {
        VStack {
            Text("Hello \(int)")
            Button("Go Forward") {
                path.append(Int.random(in: 0...100))
            }
        }.navigationTitle("\(viewModel.optionSet):\(int)")
    }
}

Add a Comment