MacOS SwiftUI: Back button in NavigationSplitView detail view

I have a Split View with the sidebar, content, and detail. Everything is working, but when I select on a NavigationLink in my detail view, the back button is seen next to the title above the content view. I want that back button to be displayed in the top bar on the left side of the detail view. I was expecting it to do this automatically, but I must be missing something.

This is where I want it to appear.

This is where it appears.

I made a simplified version of my code, because it would be too much to post but this has the same behavior.

struct TestView: View {
    
    enum SidebarSelections {
        case cycles
    }
    
    @State private var sidebarSelection: SidebarSelections = .cycles
    
    var body: some View {
        NavigationSplitView(sidebar: {
            List(selection: $sidebarSelection, content: {
                Label("Cycles", systemImage: "calendar")
                    .tag(SidebarSelections.cycles)
            })
        }, content: {
            switch sidebarSelection {
            case .cycles:
                NavigationStack {
                    List {
                        // Displayed in Content
                        NavigationLink("Cycle link", destination: {
                            // Displayed in the Detail 
                            NavigationStack {
                                List {
                                    NavigationLink("Detail Link", destination: {
                                        Text("More details")
                                    })
                                }
                            }
                        })
                    }
                }
            }
        }, detail: {
            ContentUnavailableView("Nothing to see here", systemImage: "cloud")
        })
    }
}

Is what I want to do possible? Here is a Stack Overflow post that had it working at one point.

Answered by Xavier-k in 813033022

I figured out a way to do what I want. To summarize it:

  1. Hide the navigation bar back button
  2. Create a toolbar item to work as the new back button
  3. Have it's action close your view

You can't use dismiss because it will get rid of whatever view is in the detail section of your NavigationSplitView. Instead, you must pass the variable being used to present the new view, and then set it back to false.

Lastly, your view that's equivalent to Content() needs a primaryAction toolbar item. Otherwise, the placement of toolbar items won't appear right, because it will be based on the entire toolbar and not the portion above Detail().

struct Sidebar: View {
    var body: some View {
        NavigationLink("Sidebar button", destination: {
            Content()
        })
    }
}

struct Content: View {
    var body: some View {
        NavigationLink("Content button", destination: {
            Detail()
        })
        .toolbar {
            ToolbarItem(placement: .primaryAction, content: {
                Button("Add") {
                    
                }
            })
        }
    }
}

struct Detail: View {
    
    @State private var showOtherView = false
    
    var body: some View {
        NavigationStack {
            Button("Other view") {
                showOtherView.toggle()
            }
            .navigationDestination(isPresented: $showOtherView, destination: {
                OtherView(presenting: $showOtherView)
            })
        }
    }
}

struct OtherView: View {
    
    @Binding var presenting: Bool
    
    var body: some View {
        Text("More details here")
            .navigationBarBackButtonHidden()
            .toolbar {
                ToolbarItem(placement: .destructiveAction, content: {
                    Button(action: {
                        presenting = false
                    }, label: {
                        Image(systemName: "chevron.left")
                            .foregroundStyle(.secondary)
                            .font(.title2)
                            .frame(width: 20)
                    })
                    .buttonStyle(.automatic)
                })
                
                ToolbarItem(placement: .principal, content: {
                    Text("View Title")
                        .font(.title3)
                })
            }
    }
}

#if os(macOS)
struct TestView: View {
    
    var body: some View {
        NavigationSplitView(sidebar: {
            Sidebar()
        }, content: {
            
        }, detail: {
            
        })
    }
}
#endif

Here's a photo of what it looks like.

Using the standard behavior of the NavigationSplitView works fine, but I prefer the look and feel of this better.

@Xavier-k A NavigationSplitView is a Navigation container it self, You shouldn't need to have a NavigationStack embedded in the content view. I would suggest you review NavigationSplitView. API docs and Bringing robust navigation structure to your SwiftUI app sample code, it covers various examples of how a NavigationSplitView can be configured.

Accepted Answer

I figured out a way to do what I want. To summarize it:

  1. Hide the navigation bar back button
  2. Create a toolbar item to work as the new back button
  3. Have it's action close your view

You can't use dismiss because it will get rid of whatever view is in the detail section of your NavigationSplitView. Instead, you must pass the variable being used to present the new view, and then set it back to false.

Lastly, your view that's equivalent to Content() needs a primaryAction toolbar item. Otherwise, the placement of toolbar items won't appear right, because it will be based on the entire toolbar and not the portion above Detail().

struct Sidebar: View {
    var body: some View {
        NavigationLink("Sidebar button", destination: {
            Content()
        })
    }
}

struct Content: View {
    var body: some View {
        NavigationLink("Content button", destination: {
            Detail()
        })
        .toolbar {
            ToolbarItem(placement: .primaryAction, content: {
                Button("Add") {
                    
                }
            })
        }
    }
}

struct Detail: View {
    
    @State private var showOtherView = false
    
    var body: some View {
        NavigationStack {
            Button("Other view") {
                showOtherView.toggle()
            }
            .navigationDestination(isPresented: $showOtherView, destination: {
                OtherView(presenting: $showOtherView)
            })
        }
    }
}

struct OtherView: View {
    
    @Binding var presenting: Bool
    
    var body: some View {
        Text("More details here")
            .navigationBarBackButtonHidden()
            .toolbar {
                ToolbarItem(placement: .destructiveAction, content: {
                    Button(action: {
                        presenting = false
                    }, label: {
                        Image(systemName: "chevron.left")
                            .foregroundStyle(.secondary)
                            .font(.title2)
                            .frame(width: 20)
                    })
                    .buttonStyle(.automatic)
                })
                
                ToolbarItem(placement: .principal, content: {
                    Text("View Title")
                        .font(.title3)
                })
            }
    }
}

#if os(macOS)
struct TestView: View {
    
    var body: some View {
        NavigationSplitView(sidebar: {
            Sidebar()
        }, content: {
            
        }, detail: {
            
        })
    }
}
#endif

Here's a photo of what it looks like.

Using the standard behavior of the NavigationSplitView works fine, but I prefer the look and feel of this better.

I'm pretty sure this can also be solved using isDetailLink(false).

MacOS SwiftUI: Back button in NavigationSplitView detail view
 
 
Q