Search code examples
swiftswiftuiswiftui-sheet

Sheet with dynamic size doesn't re-size correctly


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


Solution

  • 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:

    Xcode View Hierarchy Debugger

    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.