.presentationPlacement(.leading) only positions a sheet within available space; it does not constrain the sheet’s rendered width. To lock the sheet to half-width or to the primary screen pane when unfolded, attach a custom PresentationSizing modifier that bounds ProposedViewSize using geometry derived from reservedRegions.Why this happens
When presenting modal sheets across adaptable form factors, SwiftUI evaluates the window size class and display geometry to determine appropriate container dimensions. On dual-screen and foldable devices like iPhone Duo, half-open postures supply an inherent split-viewport context. In that state, the system naturally restrains the sheet container to the active half.
Once the device is completely unfolded, the window transitions into a single contiguous display canvas. At this point, standard system sheets default to their expanded multi-column or centered container sizing rules. Modifiers like .presentationPlacement(.leading) control alignment, not footprint. If the system resolves the presentation width to span the full window, an alignment rule pinned to .leading has no observable effect because the sheet already occupies the entire horizontal axis.
To control sheet width explicitly on wide and unfolded viewports, you must provide sizing constraints through .presentationSizing(...). Without a custom PresentationSizing implementation, SwiftUI delegates the sizing proposal entirely to system heuristics, which prioritize wide modal surfaces when a device is flat and fully open.
How to fix it
1. Implement a Custom PresentationSizing Struct
The PresentationSizing protocol allows you to intercept the layout engine’s proposed dimensions for a sheet container. You can clamp the horizontal proposal to a designated maximum width while leaving the vertical dimension flexible.
Create a dedicated type conforming to PresentationSizing:
import SwiftUI
struct MaxWidthPresentationSizing: PresentationSizing {
let maxWidth: CGFloat
func proposedSize(
for root: PresentationSizingRoot,
context: PresentationSizingContext
) -> ProposedViewSize {
let boundedProposal = ProposedViewSize(width: maxWidth, height: .infinity)
return ProposedViewSize(root.sizeThatFits(boundedProposal))
}
}
This struct intercepts the root presentation content and measures how it fits inside your explicit horizontal boundary. By forwarding .infinity for the height, standard vertical detents like .fraction(...) and .large remain completely functional.
2. Measure Fold Geometry on the Parent Container
To lock the sheet to the leading display pane of an unfolded device, measure the hinge or fold line using GeometryProxy. You must place the GeometryReader around the host view (the view presenting the sheet), rather than inside the sheet itself, because the presentation container does not have access to window-level seam coordinates during initial layout passes.
Inspect the reservedRegions array using the .includeInactive option. This ensures the fold boundary is returned even when the hinge is completely flat:
import SwiftUI
import MapKit
struct DualPaneSheetHost: View {
@State private var showSheet = true
@State private var sheetDetent = PresentationDetent.fraction(0.6)
var body: some View {
GeometryReader { geo in
Map()
.sheet(isPresented: $showSheet) {
SheetContent()
.interactiveDismissDisabled()
.presentationDetents(
[.fraction(0.35), .fraction(0.6), .large],
selection: $sheetDetent
)
.presentationBackgroundInteraction(
.enabled(upThrough: .fraction(0.6))
)
.presentationDragIndicator(.hidden)
.presentationSizing(
MaxWidthPresentationSizing(
maxWidth: calculatePaneWidth(from: geo)
)
)
.presentationPlacement(.leading)
}
}
}
private func calculatePaneWidth(from geo: GeometryProxy) -> CGFloat {
let regions = geo.reservedRegions(options: .includeInactive)
// Locate a vertical hinge or fold splitting the display horizontally
if let verticalSeam = regions.first(where: { $0.boundingRect.height > $0.boundingRect.width }) {
// Add leading safe area since sheet containers ignore safe area bounds
return verticalSeam.boundingRect.minX + geo.safeAreaInsets.leading
}
// Fallback for single-pane orientations or devices without seams
let totalWidth = geo.size.width + geo.safeAreaInsets.leading + geo.safeAreaInsets.trailing
return totalWidth / 2
}
}
struct SheetContent: View {
var body: some View {
NavigationStack {
List(1...20, id: \.self) { item in
Text("Navigation Item \(item)")
}
.navigationTitle("Controls")
}
}
}
3. Correct for Safe Area Insets
In SwiftUI, GeometryProxy.reservedRegions reports frames in the local coordinate space of the reader view, which is already inset by the safe area. However, modal presentation containers render across the full physical window bounds and ignore parent insets.
When calculating maxWidth from verticalSeam.boundingRect.minX, you must add geo.safeAreaInsets.leading. Failing to add this inset causes the sheet to end short of the fold line by the exact width of the display’s outer margin.
How to prevent it
- Do not rely on
.presentationPlacement(...)alone to dictate layout widths; pair it with an explicit.presentationSizing(...)constraint whenever non-default sheet widths are required. - Wrap the root screen container in your
GeometryReaderrather than the modal sheet view so display-level seam metadata is ready before presentation rendering starts. - Always pass
.includeInactivetogeo.reservedRegionswhen querying foldable metrics to capture the fold structure while fully open. - Add the leading safe area inset to your measured hinge coordinate to compensate for the sheet’s default full-bleed window coordinate system.
- Provide an explicit half-width division fallback in your sizing helper for orientations or window configurations where no vertical seam is actively reported.
FAQ
Does presentationPlacement work on standard iPhones?
On single-screen iPhones, .presentationPlacement(...) is ignored because compact size classes force sheets into full-width bottom presentations. The modifier takes effect only when the window has sufficient horizontal space, such as on iPad, Mac, and unfolded dual-screen devices.
Why can’t I place GeometryReader directly inside the sheet view?
Sheets are hosted in a separate presentation window with an isolated coordinate hierarchy. When the sheet is first evaluated, its internal GeometryReader cannot reliably resolve the physical fold location of the underlying app window, leading to timing glitches or zero-width layouts.
Can I use presentationDetents to control sheet width?
No. SwiftUI detents solely control vertical travel stops along the Y axis. Horizontal dimensioning is governed strictly by system defaults, window size classes, and the .presentationSizing(...) modifier.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
