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

2

I suspect that the view controller hosting the navigation destination is “not aware” of the sheet detents changing, because presenting the navigation destination and the detents are changed in the same view update.

If you change the detents first, then present the navigation destination with some delay, it works correctly.

Button("Navigate to First View") {
    sheetDetents = [.height(300)]
    Task {
        try await Task.sleep(for: .milliseconds(1))
        router.navigate(to: .firstNavigationView)
    }
}

Though, this means you have to change the detents in every button’s action that will present FirstNavigationView, which would not be very convenient.

You can also set the detents after the navigation destination is presented. You just need to do it in a different view update. But, for a split second the navigation destination is still going to appear wrongly laid-out, and corrects itself soon after that.

Here is a view modifier that first shows a Spacer. Only after the Spacer appears, does it change the detents, and show the actual view you want to show.

struct NavigationDestinationDetent: ViewModifier {
    @State private var appeared = false
    @Binding var detentsBinding: Set<PresentationDetent>
    let targetDetents: Set<PresentationDetent>
    
    func body(content: Content) -> some View {
        if !appeared {
            Spacer()
                .onAppear {
                    detentsBinding = targetDetents
                    appeared = true
                }
        } else {
            content
                .onDisappear {
                    appeared = false
                }
        }
    }
}

extension View {
    func navigationDetents(_ detents: Set<PresentationDetent>, bindingTo detentsBinding: Binding<Set<PresentationDetent>>) -> some View {
        modifier(NavigationDestinationDetent(detentsBinding: detentsBinding, targetDetents: detents))
    }
}

Here is a full example using this modifier:

struct ContentView: View {
    
    var body: some View {
        List {}
            .sheet(isPresented: .constant(true)) {
                SheetWithNavigation()
            }
    }
}

struct SheetWithNavigation: View {
    @State private var sheetDetents: Set<PresentationDetent> = [.height(460)]
    @State private var path: [Int] = []
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Spacer()
                Text("Sheet Presented")
                Spacer()
                
                Button("Navigate to First View") {
                    path.append(0)
                }
            }
            .navigationDestination(for: Int.self) { destination in
                switch destination {
                case 0:
                    FirstNavigationView(path: $path)
                        .navigationBarBackButtonHidden(true)
                        .navigationDetents([.height(300)], bindingTo: $sheetDetents)
                case 1:
                    Text("Foo")
                        .navigationDetents([.height(500)], bindingTo: $sheetDetents)
                default: EmptyView()
                }
            }
        }
        .background(.white)
        .presentationDetents(sheetDetents)
    }
}

struct FirstNavigationView: View {
    @Binding var path: [Int]
    
    var body: some View {
        VStack {
            Spacer()
            Text("First Navigation View")
            Spacer()
            
            Button("Go to second navigation view") {
                path.append(1)
                
            }
        }
        .background(.white)
    }
}

1

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.

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật