- Published on
- Updated Nov 8, 2026
- Tested on Xcode 27.1
- 5 min read Intermediate
> SwiftUI Sheets: presentationDetents and Multiple Sheets
// What_You_Will_Learn
- Size sheets with built-in and custom detents
- Drive a sheet's height from a selection binding
- Know which multiple-sheet setups present and which silently do nothing
Two older posts covered sheet heights and presenting more than one sheet. I merged them, built the code with Xcode 27.1 in Swift 6 mode, and logged what happened on an iPhone 17 Pro simulator running iOS 27.0. Some of the advice I'd published turned out to be wrong, so the numbers here are measured.
How Tall Detents Actually Are
presentationDetents sets the heights a sheet can rest at. A GeometryReader inside each sheet reported these heights on a 402-point-wide phone:
| Detent | Sheet height in points |
|---|---|
None, or .large | 778 |
.medium | 435 |
.fraction(0.25) | 194 |
.height(200) | 200 |
Custom, maxDetentValue / 3 | 259 |
.fraction is a fraction of the large detent, not of the screen: 25% of 778 is 194.5. .medium also isn't one fixed number. It reported 435 on its own, but 469 when it was one of several detents, so don't lay out a sheet around an exact medium height.
import SwiftUI
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Button("Show Sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
Text("Drag me")
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
}
}
Detent Order Doesn't Matter
I used to recommend listing detents from smallest to largest. That advice was meaningless, because the parameter is a Set and sets have no order. [.large, .height(100), .medium] and [.height(100), .medium, .large] both opened at 100 points, the smallest detent.
To open at a different height, or to change it from code, bind a selection. Starting at .medium opened the sheet at that detent, and assigning .large later resized it to 778:
import SwiftUI
struct SelectionExample: View {
@State private var showSheet = false
@State private var detent: PresentationDetent = .medium
var body: some View {
Button("Show Sheet") { showSheet = true }
.sheet(isPresented: $showSheet) {
Button("Go full height") { detent = .large }
.presentationDetents([.height(100), .medium, .large], selection: $detent)
}
}
}
Custom Detents
A plain static let on PresentationDetent is only a name for .height or .fraction. To calculate a height from the space available, adopt CustomPresentationDetent:
import SwiftUI
struct OneThird: CustomPresentationDetent {
static func height(in context: Context) -> CGFloat? {
context.maxDetentValue / 3
}
}
struct CustomDetentExample: View {
@State private var showSheet = false
var body: some View {
Button("Show Sheet") { showSheet = true }
.sheet(isPresented: $showSheet) {
Text("One third of the maximum")
.presentationDetents([.custom(OneThird.self)])
}
}
}
A Sheet You Can Work Behind
These modifiers compile together for a Maps-style panel. .presentationBackgroundInteraction(.enabled(upThrough: .medium)) keeps the content behind the sheet usable up to that detent, and .presentationContentInteraction(.scrolls) makes a swipe scroll the content before it resizes the sheet. The old version of this post described that last one as a safe area setting, which it isn't.
import SwiftUI
struct MapsStyleSheet: View {
@State private var showSheet = true
@State private var detent: PresentationDetent = .height(120)
var body: some View {
Color.green.opacity(0.2)
.ignoresSafeArea()
.sheet(isPresented: $showSheet) {
ScrollView {
Text("Location details")
.padding()
}
.presentationDetents([.height(120), .medium, .large], selection: $detent)
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
.presentationContentInteraction(.scrolls)
.presentationCornerRadius(20)
.interactiveDismissDisabled()
}
}
}
Presenting More Than One Sheet
The old advice was that a second .sheet modifier on the same view doesn't work reliably. Here's what I got on iOS 27.0:
| Setup | Result |
|---|---|
Two .sheet modifiers on one view, trigger the first | First sheet presents |
Two .sheet modifiers on one view, trigger the second | Second sheet presents |
Both set to true together | Only the first presents, and both bindings stay true |
| Trigger the second while the first is showing | Nothing new appears, and its binding stays true |
Second .sheet attached inside the first sheet's content | Stacks on top of the first |
sheet(item:), change the item while presented | First dismisses, onDismiss runs, then the new one presents |
So two modifiers are fine for two independent sheets that never overlap. The failure to watch for is the silent one: when a sheet can't present, its binding stays true and nothing tells you.
If one view can show several different sheets, a single sheet(item:) reads better than a row of booleans:
import SwiftUI
struct SettingsSheets: View {
enum Destination: Identifiable {
case profile
case help
var id: Self { self }
}
@State private var destination: Destination?
var body: some View {
VStack(spacing: 20) {
Button("Profile") { destination = .profile }
Button("Help") { destination = .help }
}
.sheet(item: $destination) { destination in
switch destination {
case .profile: Text("Profile")
case .help: Text("Help")
}
}
}
}
To stack one sheet on top of another, attach the second .sheet to the first sheet's content. Passing a closure down lets the top sheet close both:
import SwiftUI
struct FirstSheet: View {
@Environment(\.dismiss) private var dismissFirst
@State private var showSecond = false
var body: some View {
Button("Open Second") { showSecond = true }
.sheet(isPresented: $showSecond) {
SecondSheet {
showSecond = false
dismissFirst()
}
}
}
}
struct SecondSheet: View {
let dismissAll: () -> Void
var body: some View {
Button("Done", action: dismissAll)
}
}
Further Reading
presentationDetents(_:)andpresentationDetents(_:selection:)in the SwiftUI documentationCustomPresentationDetentin the SwiftUI documentation
// Continue_Learning
Prevent Drag-to-Dismiss on SwiftUI Sheets
Learn how to prevent users from dismissing modal sheets by swiping down in SwiftUI using interactiveDismissDisabled.
SwiftUI Form: Sections, FormStyle, and Validation
How Form differs from List, how to structure it with Sections, what .grouped and .columns actually change, and a validation pattern that does not fight the framework.
SwiftUI Animations: From Basics to KeyframeAnimator
A practical guide to SwiftUI animations covering implicit and explicit animations, spring physics, PhaseAnimator for multi-step sequences, and KeyframeAnimator for timeline-based control.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.