NavigationStack inside NavigationSplitView detail in SwiftUI

613 views Asked by At

I have a typical NavigationSplitView layout in my app:

NavigationSplitView {
  List(0..<3, selection: $selection) { item in
    Label(MainDataType.data[item].name, systemImage: MainDataType.data[item].icon)
  }
} content: {
  switch selection {
     case 0: StudentsView()
     case 1: WorksView()
// More options here...
  }
} detail: {
  NoDataView() // <- Just for main view first presentation.
}

Sidebar and Content views work perfectly fine. For Detail view, I overwrite default NoDataView with, for example, StudentDetail, from StudentsView selection:

struct StudentsView() {
...
List(students, id:\.self) { student in
   NavigationLink(destination: NavigationStack { // <- In order to get navigation inside SplitView detail column.
     StudentDetailView(student: student)}) {
       StudentCellView(student: student)
...

As in StudentDetailView I have deeper NavigationLinks (see attached gif).

This layout has been working until iOS17. Now it still works almost in every case, but this console message appears:

A NavigationLink is presenting a NavigationStack onto a column. This construction is not supported. Consider instead just presenting a new root view which will replace that of an existing stack in the column, or else the column itself if there is no NavigationStack in the target column. If there is a stack in the target column, appending to that stack's path is another option.

I'm testing these navigation changes:

struct StudentsView() {
...
  List(students, id:\.self) { student in
    NavigationLink(destination: StudentDetailView(student: student)) {
       StudentCellView(student: student)
...

This way no console message is shown, and deep NavigationLinks work (more or less), but with no navigation behavior (navigating back, animation...).

So I'm now struggling with this two questions:

  • Is there any way of getting a complete full-support NavigationStack inside detail view of NavigationSplitView?
  • If so, what's the code to get it work?
  • If not, what are the alternatives?

enter image description here

2

There are 2 answers

2
vollkorntomate On

Your example looks a little like the three-column example from Apple.

In general, you would want to have your NavigationStack as the root view in the details part of the split view. Inside the NavigationStack, you would then choose which view should be displayed, based on what has been selected in the content part. This is exactly what the part in the error message means:

Consider instead just presenting a new root view which will replace that of an existing stack in the column

Here is a minimal example of what I mean:

struct TestView: View {
    enum ContentSelection: Hashable {
        case one
        case two
    }
    
    @State var contentSelection: ContentSelection?
    @State var columnVisibility: NavigationSplitViewVisibility = .doubleColumn
    
    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            List {
                Text("Sidebar one")
            }
        } content: {
            List(selection: $contentSelection) {
                Text("Content one")
                    .tag(ContentSelection.one)
                Text("Content two")
                    .tag(ContentSelection.two)
            }
        } detail: {
            NavigationStack {
                switch contentSelection {
                case .one:
                    NavigationLink("ONE") {
                        Text("ONE one level deeper")
                    }
                case .two:
                    NavigationLink("TWO") {
                        Text("TWO one level deeper")
                    }
                case .none:
                    Text("Nothing")
                }
            }
        }
    }
}

Preview of the example code

In your case, you would have your students list in the content part, but you would need to replace the NavigationLinks in the list by using the List(selection:) initializer. Based on this selection, you can display the correct student details in the details section (inside the NavigationStack there).

Also, for deeper navigation hierarchies, you can take a look at NavigationStack(path:root:), which works quite nicely together with the NavigationLink(value:label:) and the .navigationDestination(for:destination:) modifier. You can just create your own Destination enum (make it conform to Hashable) and use an array ([Destination]) as your path.

0
arroyot24 On

Voila! I have found the problem. So simple: just embedding detail: column content itself in a NavigationStack.

NavigationSplitView {
  List(0..<3, selection: $selection) { item in
    Label(MainDataType.data[item].name, systemImage: MainDataType.data[item].icon)
  }
} content: {
  switch selection {
     case 0: StudentsView()
     case 1: WorksView()
// More options here...
  }
} detail: {
  NavigationStack {  // <- Here!
     NoDataView()
  }
}

Despite detail: content is being in fact defined on intermediate views, this way general NavigationSplitView outline seems to keep its behavior app-wide. So does every stack of views displayed on detail: column.