DocumentBrowser toolbar behavior in SwiftUI apps

I’m building a document-based SwiftData app (iPhone/iPad/Mac). Here’s a minimal example of how I’m using DocumentGroup.

DocumentGroup(editing: Trip.self, contentType: .trips) {
     ContentView()
}

if #available(iOS 18.0, *) {
     DocumentGroupLaunchScene {
          NewDocumentButton("New Trip")
     }
}

I’m struggling with the toolbar behavior in DocumentGroup apps. My content view uses a TabView, and each tab contains a NavigationSplitView. After I select a document in the document browser, I see my tabs. Regardless of which tab is selected, there’s a navigation bar showing the document name and a back button to the document browser. However, only the first tab shows the disclosure button to rename the document. I’d expect to be able to rename the document anywhere the name is shown.

When I navigate to the detail view of my NavigationSplitView (or when using NavigationView/NavigationStack), I still see that back button to the document browser. When the user taps it, they expect to go back to the previous view, not to the document browser.

What’s really odd is that even sheet or fullScreenCover presentations include these document UI elements in the navigation bar. I can’t get rid of them. Even if I set a title via the toolbar or navigationTitle, the rename disclosure button remains visible.

Do DocumentGroup apps intentionally show their specific navigation bar everywhere? Is this a bug or expected behavior? And is it expected that the rename disclosure button appears only on the first tab of a TabView?

Answered by DTS Engineer in 854457022

The back (<) navigation item that allows a user to navigate back to the launch scene, together with the navigation item title that shows the document name and the disclosure button, should always be there when you are on the TabView.

For a tab that has navigation stack, if you navigate to the detail view, the navigation bar should show the items of detail.

Using the following code as an example:

struct ContentView: View {
    var body: some View {
        TabView {
            Tab("Rectangle", systemImage: "rectangle") {
                NavigationStack {
                    NavigationLink("Tap to see details") {
                        Text("Details")
                            .toolbar {
                                ToolbarItem(placement: .topBarTrailing) {
                                    Button("Add") {
                                        print("Button pressed.")
                                    }
                                }
                            }
                    }
                }
            }
            Tab("Circle", systemImage: "circle") {
                NavigationStack {
                    Text("Circle")
                        .toolbar {
                            ToolbarItem(placement: .topBarTrailing) {
                                Button("Remove") {
                                    print("Button pressed.")
                                }
                            }
                        }
                }
            }
        }
    }
}

If you tap the link in the first tab, the UI will go to the detail view, and the navigation bar shows a back (<) navigation item and the "Add" toolbar item. Tapping the back navigation item goes back to view that has the link, which is the top level view of the navigation stack.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

The back (<) navigation item that allows a user to navigate back to the launch scene, together with the navigation item title that shows the document name and the disclosure button, should always be there when you are on the TabView.

For a tab that has navigation stack, if you navigate to the detail view, the navigation bar should show the items of detail.

Using the following code as an example:

struct ContentView: View {
    var body: some View {
        TabView {
            Tab("Rectangle", systemImage: "rectangle") {
                NavigationStack {
                    NavigationLink("Tap to see details") {
                        Text("Details")
                            .toolbar {
                                ToolbarItem(placement: .topBarTrailing) {
                                    Button("Add") {
                                        print("Button pressed.")
                                    }
                                }
                            }
                    }
                }
            }
            Tab("Circle", systemImage: "circle") {
                NavigationStack {
                    Text("Circle")
                        .toolbar {
                            ToolbarItem(placement: .topBarTrailing) {
                                Button("Remove") {
                                    print("Button pressed.")
                                }
                            }
                        }
                }
            }
        }
    }
}

If you tap the link in the first tab, the UI will go to the detail view, and the navigation bar shows a back (<) navigation item and the "Add" toolbar item. Tapping the back navigation item goes back to view that has the link, which is the top level view of the navigation stack.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Hi Ziqiao,

thanks for the code snippet — it also works well in my app. I only made a few small changes:

I’m using NavigationSplitView instead of NavigationStack, and placing the List inside the navigation view slightly changes the appearance of the navigation bar. It now returns directly from the detail view to the document browser.

Also, the sheets and full-screen covers you present should now show the document title and back buttons in their navigation bars.

  • Xcode 16.4
  • iOS 18.3
  • iPhone 18 Pro (Simulator)
struct ContentView: View {
    private enum Row: Hashable { case details }

    @State private var selection: Row?

    var body: some View {
        TabView {
            Tab("Rectangle", systemImage: "rectangle") {
                NavigationSplitView {
                    List(selection: $selection) {
                        NavigationLink(value: Row.details) {
                            Text("Tap to see details")
                        }
                    }
                    .toolbar {
                        ToolbarItem(placement: .topBarTrailing) {
                            Button("Add") {
                                print("Button pressed.")
                            }
                        }
                    }
                } detail: {
                    switch selection {
                    case .details:
                        Text("Details")
                    default:
                        Text("Select something")
                    }
                }
            }
            Tab("Circle", systemImage: "circle") {
                NavigationStack {
                    Text("Circle")
                        .toolbar {
                            ToolbarItem(placement: .topBarTrailing) {
                                Button("Remove") {
                                    print("Button pressed.")
                                }
                            }
                        }
                }
            }
        }
    }
}

Thanks for sharing more details. I'm not a UI designer, but embedding a NavigationSplitView into a TabView isn't a common pattern to me, because both of the views are intended to be used as the top level container.

I'd use NavigationStack when creating a navigation hierarchy in a tab, or use a segmented picker when creating a tab-style UI in the list or detail view of NavigationSplitView.

Having said that, if you really need to embed a NavigationSplitView into a TabView, please feel free to file a feedback report with your concrete use case for the UI framework folks to consider.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Hi Ziqiao,

the problem is that when you want to use SwiftData for an enterprise app, the user needs proper navigation. In such apps, you usually have more than one screen or form. I did some additional tests with your code and replaced the NavigationStack with a NavigationView. The reason is that I need the automatic two-column split for a sidebar menu and a detail view, not just a single view.

The NavigationSplitView doesn’t seem to work as expected. Using it as a root view with an embedded TabView was my first attempt, but that didn’t work either. That’s why I tried embedding the NavigationSplitViews inside the TabView. When I replace your NavigationStack with a NavigationView, I get the automatic two-column view on iPad — but with a second navigation view below the DocumentGroup navigation view. This looks bugged.

The NavigationSplitView also has issues with the back button and the document title in sheets. I’ll file a feedback report for now as you suggested and try to find a workaround for my app, maybe with a custom sidebar built as an HStack.

But thanks for your help with this problem.

DocumentBrowser toolbar behavior in SwiftUI apps
 
 
Q