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:
P.S.: I'm using iOS 17.2 as the deployment target.
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
.