presentationSizing(.form.fitted(horizontal: false, vertical: true)) with a NavigationStack for native toolbars, the fitted height collapses because NavigationStack returns a minimal size during layout. Fix it by removing the NavigationStack wrapper, adding top padding to your content, and overlaying a NavigationStack containing only Color.clear with the toolbar. For scrollable content, constrain the overlay’s frame height and align it top so gestures pass through.Why this happens
The presentationSizing(_:) modifier introduced in iOS/iPadOS 18 lets you control a sheet’s size. The .fitted(horizontal:vertical:) option asks the content for its ideal size via PresentationSizingRoot.sizeThatFits. This works when the sheet’s root view is a simple VStack or ScrollView.
When you wrap the content in a NavigationStack to get a native toolbar, the sizing logic breaks. NavigationStack returns a very small height from sizeThatFits regardless of its internal content. The presentation system then sizes the sheet to that tiny height, clipping or hiding your content. This occurs on iPadOS 18 through iPadOS 27 and affects both .form and .page presentation styles.
How to fix it
1. Replace the NavigationStack wrapper with an overlay (read-only content)
Move the NavigationStack out of the sheet’s root view and into an overlay. Use Color.clear as the NavigationStack’s content so it contributes no intrinsic size. Add top padding to your actual content equal to the toolbar height (64 points) so the toolbar doesn’t overlap the first line.
private let topPadding: CGFloat = 64
private var sheetContentWithNativeToolbar: some View {
mainContent
.padding(.top, topPadding)
.overlay {
NavigationStack {
Color.clear
.toolbarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(role: .close) { sheetKind = nil }
}
ToolbarItem(placement: .title) {
Text("Sheet with native toolbar")
.font(.title)
}
}
}
}
}
The sheet root is now mainContent (a ScrollView), so sizeThatFits measures the scrollable text correctly. The overlay NavigationStack expands to fill the sheet and draws the toolbar at the top.
2. Allow gestures through for scrollable/interactive content
The full-screen overlay blocks touches to the content underneath. If your content scrolls or contains buttons, restrict the overlay to the header area only:
private var sheetContentWithNativeToolbar: some View {
mainContent
.padding(.top, topPadding)
.overlay(alignment: .top) {
NavigationStack {
Color.clear
.toolbarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(role: .close) { sheetKind = nil }
}
ToolbarItem(placement: .title) {
Text("Sheet with native toolbar")
.font(.title)
}
}
}
.frame(height: topPadding)
}
}
Adding .frame(height: topPadding) and alignment: .top limits the NavigationStack to the 64-point header band. The rest of the sheet receives gestures normally, so scrolling works.
3. Keep toolbar buttons at the top on iPhone Duo
On iPhone Duo the cancellation button may appear below the camera cutout, outside the constrained overlay frame. Force it to stay at the top with .axisBehavior(.horizontalOnly) on the toolbar:
NavigationStack {
Color.clear
.toolbarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(role: .close) { sheetKind = nil }
}
ToolbarItem(placement: .title) {
Text("Sheet with native toolbar")
.font(.title)
}
}
}
.toolbar(.axisBehavior(.horizontalOnly))
This keeps the button in the top toolbar area on all device form factors.
How to prevent it
- Avoid wrapping sheet content in NavigationStack when using
.presentationSizing(.fitted). Use the overlay pattern from the start. - Measure toolbar height once (64 pt for inline title display mode) and apply it as top padding; don’t rely on GeometryReader or dynamic measurement.
- Test on iPad and iPhone Duo simulators — the toolbar position differs and can hide buttons if the overlay frame is too short.
- Prefer
.formstyle for fitted sheets;.pagestyle may still size to full screen on iPhone regardless of the fitted setting.
FAQ
Why does NavigationStack return a near-zero height during sizeThatFits?
NavigationStack is a container that defers its layout to the navigation bar and its child views. During the presentation sizing pass, the child hasn’t been laid out yet, so the stack reports only the minimal chrome height. This is a known limitation of the current PresentationSizing implementation.
Can I use a custom PresentationSizing implementation instead?
You could implement a custom PresentationSizing that ignores the NavigationStack and measures the inner content directly, but it requires replicating the internal sizeThatFits logic and is fragile across OS versions. The overlay workaround is simpler and uses only public APIs.
Does this affect macOS or visionOS sheets?
The issue is specific to iPadOS/iOS 18+ where presentationSizing(.fitted) is available. macOS sheets use different sizing mechanics, and visionOS uses presentationSizing but doesn’t exhibit the NavigationStack collapse in the same way. Test each platform separately if you share sheet code.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
