BS
BleepingSwift
Published on
Updated Nov 8, 2026
Tested on Xcode 27.1
5 min read
Intermediate

> SwiftUI Sheets: presentationDetents and Multiple Sheets

Share:

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

DetentSheet height in points
None, or .large778
.medium435
.fraction(0.25)194
.height(200)200
Custom, maxDetentValue / 3259

.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.

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

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

Swift
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.

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

SetupResult
Two .sheet modifiers on one view, trigger the firstFirst sheet presents
Two .sheet modifiers on one view, trigger the secondSecond sheet presents
Both set to true togetherOnly the first presents, and both bindings stay true
Trigger the second while the first is showingNothing new appears, and its binding stays true
Second .sheet attached inside the first sheet's contentStacks on top of the first
sheet(item:), change the item while presentedFirst 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:

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

Swift
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

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.