NavigationStack inside NavigationSplitView on Mac

I've seen several posts regarding NavigationStack in a NavigationSplitView. All had specific issues and some were marked as resolved. I couldn't get any of the suggested solutions working on macOS so I'll present some stripped down examples, all part of FB11842563:

1. Sidebar Selection

struct SidebarSelection: View {

    @State var selection: Int?
    @State var path: [Int] = []

    var body: some View {
        NavigationSplitView {
            List(1...20, id: \.self, selection: $selection) { number in
                    Text("I like \(number)")
            }
        } detail: {
            NavigationStack(path: $path) {
                VStack {
                    Image(systemName: "x.squareroot")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)
                    Text("This is the NavigationStack root")
                }
                .padding()
                .navigationDestination(for: Int.self) { number in
                    Text("You chose \(number)")
                }
            }
        }
        .onChange(of: selection) { newValue in
            print("You clicked \(newValue)")
            if let newValue {
                path.append(newValue)
            }
        }
        .onChange(of: path) { newValue in
            print("Path changed to \(path)")
        }
    }
}

If we run this and click:

  1. „I like 5“
  2. „I like 6“
  3. „I like 7“

We would expect the detail view to show:

  1. „You chose 5“
  2. „You chose 6“
  3. „You chose 7“

And the console to show:

You clicked Optional(5)
Path changed to [5]
You clicked Optional(6)
Path changed to [5, 6]
You clicked Optional(7)
Path changed to [5, 6, 7]

What we actually see in the detail view is:

  1. „You chose 5“
  2. „This is the NavigationStack root“
  3. „You chose 7“

And the console shows:

Update NavigationRequestObserver tried to update multiple times per frame.
You clicked Optional(5)
Path changed to [5]
You clicked Optional(6)
Path changed to []
You clicked Optional(7)
Path changed to [7]

2. Sidebar and Stack Selection

Now we copy the list to the navigationDestination:

struct SidebarAndStackSelection: View {

    @State var selection: Int?
    @State var path: [Int] = []

    var body: some View {
        NavigationSplitView {
            List(1...20, id: \.self, selection: $selection) { number in
                    Text("I like \(number)")
            }
        } detail: {
            NavigationStack(path: $path) {
                VStack {
                    Image(systemName: "x.squareroot")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)
                    Text("This is the NavigationStack root")
                }
                .padding()
                .navigationDestination(for: Int.self) { number in
                    VStack {
                        Text("You chose \(number)")
                        List(1...20, id: \.self, selection: $selection) { number in
                            Text("I like \(number)")
                        }
                    }
                }
            }
        }
        .onChange(of: selection) { newValue in
            print("You clicked \(newValue)")
            if let newValue {
                path.append(newValue)
            }
        }
        .onChange(of: path) { newValue in
            print("Path changed to \(path)")
        }
    }
}

We repeat our test from above, clicking either on the sidebar or in the detail view and we expect the same outcome. This time the detail view shows the expected screen and the path is not completely wiped out but it is also not appended:

Update NavigationRequestObserver tried to update multiple times per frame.
You clicked Optional(5)
Path changed to [5]
You clicked Optional(6)
Path changed to [6]
You clicked Optional(7)
Path changed to [7]

3. Sidebar and Stack Selection, initialized

Same as before, but now we initialize the view with a non-empty path:

SidebarAndStackSelection(path: [1])

The app freezes on launch, CPU is at 100 percent and the console shows only:

Update NavigationRequestObserver tried to update multiple times per frame.
Update NavigationRequestObserver tried to update multiple times per frame.

The SwiftUI instruments seem to show heavy activity of the Stack and the SplitView:

4. Selection only in Stack

Once we remove the selection from the sidebar everything works as expected (adding the NavigationStack to the root view to be able to click on a number):

struct SidebarWithoutSelectionButStack: View {
    
    @State var selection: Int?
    @State var path: [Int] = []
    
    var body: some View {
        NavigationSplitView {
            List(1...20, id: \.self) { number in
                    Text("I like \(number)")
            }
        } detail: {
            NavigationStack(path: $path) {
                List(1...20, id: \.self, selection: $selection) { number in
                    Text("I like \(number)")
                }
                .padding()
                .navigationDestination(for: Int.self) { number in
                    VStack {
                        Text("You chose \(number)")
                        List(1...20, id: \.self, selection: $selection) { number in
                            Text("I like \(number)")
                        }
                    }
                }
            }
        }
        .onChange(of: selection) { newValue in
            print("You clicked \(newValue)")
            if let newValue {
                path.append(newValue)
            }
        }
        .onChange(of: path) { newValue in
            print("Path changed to \(path)")
        }
    }
}

Problem of course is, that now the sidebar is useless.

Post not yet marked as solved Up vote post of dominik-mayer Down vote post of dominik-mayer
1.2k views

Replies

5. Buttons in the Sidebar

We could add buttons to the sidebar:

struct SidebarButtonsAndStackSelection: View {
    
    @State var selection: Int?
    @State var path: [Int] = []
    
    var body: some View {
        NavigationSplitView {
            List(1...20, id: \.self) { number in
                Button("I like \(number)") {
                    path.append(number)
                }
            }
        } detail: {
            NavigationStack(path: $path) {
                List(1...20, id: \.self, selection: $selection) { number in
                        Text("I like \(number)")
                }
                .padding()
                .navigationDestination(for: Int.self) { number in
                    VStack {
                        Text("You chose \(number)")
                        List(1...20, id: \.self, selection: $selection) { number in
                            Text("I like \(number)")
                        }
                    }
                }
            }
        }
        .onChange(of: selection) { newValue in
            print("You clicked \(newValue)")
            if let newValue {
                path.append(newValue)
            }
        }
        .onChange(of: path) { newValue in
            print("Path changed to \(path)")
        }
    }
}

This initially works but after clicking around a bit the CPU goes up to 100 percent and the app freezes.

Am I doing anything wrong? Or is the combination of NavigationStack and NavigationSplitView super buggy?

Exact same issue on my end

I'm seeing the same issue too. I suspect I'll have to ditch NavigationPath for now - even in iOS 16 it seems unusable with NavigationSplitView. (FWIW I've tried a separate NavigationPath for each of list item, but still behaves oddly.)

Having the same issue. Has anyone found a solution in the meantime?

Jumping in to fan the fire - also seeing this issue!

Hi all,

Thanks so much for bringing this up. This is a known issue. Please file feedback at https://feedbackassistant.apple.com since that always is of help! If you do, post that FB number here as @dominik-mayer has done.