swiftswiftuiswiftui-navigationstackswiftui-toolbar

NavigationStack clipping contents of .toolbar modifier


When using a custom .toolbar within a NavigationStack, the contents of the toolbar are getting clipped on navigation-pop. The code below first renders the view as expected (without any clipping). But as soon as a new destination is pushed onto the stack, and then popped back via the built-in Back action, the clipping occurs. Everything looks correct on initial load and during the nav transition, but when the transition is finished, the issue occurs.

I followed the official documentation to construct this hierarchy, but does anyone see anything obviously wrong with the code? Or is this a bug in SwiftUI?

struct PlaygroundView: View {
    @State var destinations: [Color] = []
    
    var body: some View {
        NavigationStack(path: $destinations) {
            Color.blue
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        VStack {
                            Text("First Line Title")
                                .font(.title)
                                .foregroundStyle(.primary)
                            Text("Second Line Subtitle")
                                .font(.title2)
                                .foregroundStyle(.secondary)
                        }
                    }
                }
                .navigationDestination(for: Color.self, destination: { color in
                    color
                })
                .onTapGesture {
                    destinations.append(.red)
                }
        }
    }
}

This is what it looks like in practice:

NavigationStack Toolbar Bug

P.S.: I'm using iOS 17.2 as the deployment target.


Solution

  • You should not put large views like this in a navigation bar. You cannot control the size of the built-in navigation bar.

    I would suggest doing .toolbar(.hidden, for: .navigationBar) and writing your own navigation bar with .safeAreaInset(edge: .top) { ... } if you want it to have a custom appearance.

    By inspecting the view hierarchy, the view is clipped because one of the subviews of the UINavigationBar has clipsToBounds = true. Therefore, as a hack, you can get access to the UINavigationBar with a UIViewControllerRepresentable, and never allow clipsToBounds to be set to true.

    import Combine
    
    struct NavBarNoClip: UIViewControllerRepresentable {
    
        func makeUIViewController(context: Context) -> UIViewController {
              ViewController()
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        }
    
        private class ViewController: UIViewController {
            var cancellables: Set<AnyCancellable> = []
    
            override func viewWillAppear(_ animated: Bool) {
                super.viewWillAppear(animated)
                guard let navBar = navigationController?.navigationBar, cancellables.isEmpty else {
                    return
                }
                for subview in navBar.subviews {
                    subview.publisher(for: \.clipsToBounds).sink { clipped in
                        if clipped {
                            subview.clipsToBounds = false
                        }
                    }
                    .store(in: &cancellables)
                }
            }
        }
    }
    

    Then you can put .background { NavBarNoClip() } on the Color.blue or any view inside the NavigationStack.