I have a sheet with a NavigationStack and I'm trying to change the height dynamically. For some reason when I change the height, the view doesn't show the content properly, however if I scrolling the sheet, the view change and show its content correctly.
struct SheetWithNavigation: View {
@EnvironmentObject var router: Router
@State private var sheetDetents: Set<PresentationDetent> = [.height(460)]
var body: some View {
NavigationStack(path: $router.navigation) {
VStack {
Spacer()
Text("Sheet Presented")
Spacer()
Button("Navigate to First View") {
router.navigate(to: .firstNavigationView)
}
}
.navigationDestination(for: Router.Destination.self) { destination in
switch destination {
case .firstNavigationView:
FirstNavigationView()
.navigationBarBackButtonHidden(true)
.onAppear {
sheetDetents = [.height(300)]
}
}
}
}
.background(.white)
.presentationDetents(sheetDetents)
}
}
struct FirstNavigationView: View {
var body: some View {
VStack {
Spacer()
Text("First Navigation View")
Spacer()
Button("Go to second navigation view") {}
}
.background(.white)
}
}
This is a video evidence about the behavior
This seems to be a bug that occurs when embedding a navigation stack in a sheet presentation under iOS 17. I was no longer able to reproduce the bug with iOS 18, i.e. Apple seems to have partially fixed it.
The problematic layout behavior can be observed in detail if the views are wrapped with a custom Layout. This reveals that the FirstNavigationView
is not actually aware of new view height caused by the manual programmatic change of the detent.
If you use the view hierarchy debugger, this also becomes clearly visible:
Considering that SwiftUI internally uses a UIHostingController
that is embedded in a UINavigationControoler
, which in turn is embedded in a presentation controller, it is almost surprising that this constellation does not cause even more problems.
My recommendation for a solution would therefore be to try to completely eliminate the NavigationStack
and thus simplify the problem. In your demo code you hide the back button and maybe you don't need the features of a NavigationStack
at all.
Unless you need the advanced features of a NavigationStack
, this can be implemented relatively easily and leanly:
struct SheetWithNavigation: View {
enum Destination {
case rootView
case firstNavigationView
}
@State private var destination: Destination = .rootView
@State private var sheetDetents: Set<PresentationDetent> = [.height(460)]
var body: some View {
VStack {
switch destination {
case .rootView:
VStack {
Spacer()
Text("Sheet Presented")
Spacer()
Button("Navigate to First View") {
destination = .firstNavigationView
}
}
case .firstNavigationView:
FirstNavigationView()
.navigationBarBackButtonHidden(true)
.task {
sheetDetents = [.height(300)]
}
}
}
.background(.white)
.presentationDetents(sheetDetents)
}
}
With this solution, the problem does not occur under iOS 17.
If you are dependent on the functions of a navigation controller, you could also try adding your own UINavigationController
to your SwiftUI view hierarchy via a UIViewControllerRepresentable
implementation. However, the implementation would be much more complicated.