SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working

I'm playing around with the new navigation API's offered in ipadOS16/macOS13, but having some trouble working out how to combine NavigationSplitView, NavigationStack and NavigationLink together on macOS 13 (Testing on a Macbook Pro M1). The same code does work properly on ipadOS.

I'm using a two-column NavigationSplitView. Within the 'detail' section I have a list of SampleModel1 instances wrapped in a NavigationStack. On the List I've applied navigationDestination's for both SampleModel1 and SampleModel2 instances.

When I select a SampleModel1 instance from the list, I navigate to a detailed view that itself contains a list of SampleModel2 instances. My intention is to navigate further into the NavigationStack when clicking on one of the SampleModel2 instances but unfortunately this doesn't seem to work. The SampleModel2 instances are selectable but no navigation is happening.

When I remove the NavigationSplitView completely, and only use the NavigationStack the problem does not arise, and i can successfully navigate to the SampleModel2 instances.

Here's my sample code:


// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
    let id = UUID()
    
    static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}

struct SampleModel2: Hashable, Identifiable {
    let id = UUID()
    
    static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}


// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
    
    enum NavItem {
        case first
    }
    
    var body: some View {
        NavigationSplitView {
            NavigationLink(value: NavItem.first) {
                Label("First", systemImage: "house")
            }
        } detail: {
            SampleListView()
        }
    }
}

// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
    
    @State var path = NavigationPath()
    @State var selection: SampleModel1.ID? = nil
    
    var body: some View {
        NavigationStack(path: $path) {
            List(SampleModel1.samples, selection: $selection) { model in
                NavigationLink("\(model.id)", value: model)
            }
            .navigationDestination(for: SampleModel1.self) { model in
                SampleDetailView(model: model)
            }
            .navigationDestination(for: SampleModel2.self) { model in
                Text("Model 2 ID \(model.id)")
            }
        }
    }
}

// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
    
    var model: SampleModel1
    
    var body: some View {
        Text("Model 1 ID \(model.id)")
        
        List (SampleModel2.samples) { model2 in
            NavigationLink("\(model2.id)", value: model2)
        }
    }
}




struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

(NOTE: also created this issue on stackoverflow here)

Also created a feedback assistant item: FB10876902

Answered by nkalvi in 723022022

Here's a workaround, updating the NavigationPath using the selection in the DetailView; this works under iPadOS and MacOS.

import SwiftUI

// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
    let id = UUID()
    
    static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}

struct SampleModel2: Hashable, Identifiable {
    let id = UUID()
    
    static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}


// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
    @State var path = NavigationPath()
    
    enum NavItem: Hashable, Equatable {
        case first
    }
    
    var body: some View {
        NavigationSplitView {
            List {
                NavigationLink(value: NavItem.first) {
                    Label("First", systemImage: "house")
                }
            }
        } detail: {
            SampleListView(path: $path)
        }

    }
}

// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
    // Get the selection from DetailView and append to path
    // via .onChange
    @State var selection2: SampleModel2? = nil
    @Binding var path: NavigationPath
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Text("Path: \(path.count)")
                    .padding()
                List(SampleModel1.samples) { model in
                    NavigationLink("Model1: \(model.id)", value: model)
                }
                .navigationDestination(for: SampleModel2.self) { model in
                    Text("Model 2 ID \(model.id)")
                        .navigationTitle("navigationDestination(for: SampleModel2.self)")
                }
                .navigationDestination(for: SampleModel1.self) { model in
                    SampleDetailView(model: model, path: $path, selection2: $selection2)
                        .navigationTitle("navigationDestination(for: SampleModel1.self)")
                }
                .navigationTitle("First")
            }
            .onChange(of: selection2) { newValue in
                path.append(newValue!)
            }
        }
    }
}

// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
    
    var model: SampleModel1
    @Binding var path: NavigationPath
    @Binding var selection2: SampleModel2?
    
    var body: some View {
        NavigationStack {
            Text("Path: \(path.count)")
                .padding()
            List(SampleModel2.samples, selection: $selection2) { model2 in
                NavigationLink("Model2: \(model2.id)", value: model2)
                // This also works (without .onChange):
                //                Button(model2.id.uuidString) {
                //                    path.append(model2)
                //                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


The problem is there even in Apple's sample Bringing robust navigation structure to your SwiftUI app | Apple Developer Documentation: on Mac Related Recipes cannot be selected after choosing Experience > Two Columns > Dessert > Apple Pie.

Accepted Answer

Here's a workaround, updating the NavigationPath using the selection in the DetailView; this works under iPadOS and MacOS.

import SwiftUI

// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
    let id = UUID()
    
    static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}

struct SampleModel2: Hashable, Identifiable {
    let id = UUID()
    
    static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}


// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
    @State var path = NavigationPath()
    
    enum NavItem: Hashable, Equatable {
        case first
    }
    
    var body: some View {
        NavigationSplitView {
            List {
                NavigationLink(value: NavItem.first) {
                    Label("First", systemImage: "house")
                }
            }
        } detail: {
            SampleListView(path: $path)
        }

    }
}

// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
    // Get the selection from DetailView and append to path
    // via .onChange
    @State var selection2: SampleModel2? = nil
    @Binding var path: NavigationPath
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Text("Path: \(path.count)")
                    .padding()
                List(SampleModel1.samples) { model in
                    NavigationLink("Model1: \(model.id)", value: model)
                }
                .navigationDestination(for: SampleModel2.self) { model in
                    Text("Model 2 ID \(model.id)")
                        .navigationTitle("navigationDestination(for: SampleModel2.self)")
                }
                .navigationDestination(for: SampleModel1.self) { model in
                    SampleDetailView(model: model, path: $path, selection2: $selection2)
                        .navigationTitle("navigationDestination(for: SampleModel1.self)")
                }
                .navigationTitle("First")
            }
            .onChange(of: selection2) { newValue in
                path.append(newValue!)
            }
        }
    }
}

// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
    
    var model: SampleModel1
    @Binding var path: NavigationPath
    @Binding var selection2: SampleModel2?
    
    var body: some View {
        NavigationStack {
            Text("Path: \(path.count)")
                .padding()
            List(SampleModel2.samples, selection: $selection2) { model2 in
                NavigationLink("Model2: \(model2.id)", value: model2)
                // This also works (without .onChange):
                //                Button(model2.id.uuidString) {
                //                    path.append(model2)
                //                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


Is this still working for you? When I click on a link in the detail view the app freezes and CPU goes up to 100 percent. If I read the SwiftUI instruments correctly the NavigationStack is constantly redrawn.

SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working
 
 
Q