Sheets

Last updated on 27 Oct 2024.

Written by Jia Chen.

Sheets

Last updated on 27 Oct 2024.

Written by Jia Chen.

Sheets

Last updated on 27 Oct 2024.

Written by Jia Chen.

Scroll Down

On this page

On this page

Presenting Sheets

Sheets are modals that present themselves over the current context. To present a sheet, you need two components.

  • A condition to present the sheet

  • The Sheet's contents

The condition can come in the 2 forms—a Binding Boolean value or an Optional item.

In the example below, a isNewItemSheetPresented Boolean State variable can be set to true to present the sheet containing NewItemView.

struct ContentView: View {
  
    @State private var isNewItemSheetPresented = false
  
    var body: some View {
        Button("Create New Item") {
            isNewItemSheetPresented = true
        }
        .sheet(isPresented: $isNewItemSheetPresented) {
            NewItemView()
        }
        
    }
}

Using a Binding Boolean

When the Binding Boolean value is set to true, the sheet will be presented. When the Binding Boolean is set to false, the sheet will be dismissed.

To present a sheet using a Binding Boolean, you will need

  1. A Boolean State variable to manage the presentation of the sheet.

  2. To add the .sheet modifier to a view that will be visible view.

  3. To pass that variable as a Binding value into the sheet

  4. To set the variable to true, in order to present it

  5. To supply the sheet with a view to display as its contents.

@State private var isSheetPresented = false
Button("Show Sheet") {
    isSheetPresented.toggle()
}
.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
}

Using an Binding Optional Item

When using an Optional item, if the value is non-nil, the sheet will be presented. When the Binding Optional value is set to nil, the sheet will be dismissed.

To present a sheet using a Binding Optional value, you will need

  1. A State variable of an Optional, Identifiable value to manage the presentation of the sheet.

  2. To add the .sheet modifier to a view that will be visible view.

  3. To pass that variable as a Binding Optional value into the sheet.

  4. To set the Optional value to an item, in order to present it.

  5. To supply the sheet with a view to display as its contents.

In the following example, the user can select a contact from a contacts list and view more information in a sheet.

  • Contact is a struct conforming to the Identifiable protocol

  • A State variable, presentedContact is created to manage the sheet's presentation

  • When the presentedContact value is non-nil, the sheet will appear

  • The sheet's contents is set to a custom ContactDetailView which accepts a Contact value.

  • The sheet's contents closure allows you to access an unwrapped Contact value to be passed into the ContactDetailView.

@State private var presentedContact: Contact
Button("Show Contact Details") {
    presentedContact = contact
}
.sheet(item: $presentedContact) { contact in
    ContactDetailView(contact: contact)
}

On Dismiss

You can execute code when the sheet is dismissed by supplying the sheet modifier with the on dismiss closure.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
} onDismiss: {
    print("Bye!")
}
.sheet(item: $presentedContact) { contact in
    ContactDetailView(contact: contact)
} onDismiss: {
    print("Bye!")
}

Dismissing from a Separate View

It is not uncommon to use an entirely separate view to compose the sheet's contents. From a separate view, you can dismiss the sheet with the dismiss environment value.

To do this,

  1. Create a dismiss environment value

    @Environment(\.dismiss) var dismiss
  2. When you want to dismiss the sheet, you can call dismiss().

.sheet(isPresented: $isSheetPresented) {
    OtherView()
}
struct OtherView: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Button("Dismiss") {
            dismiss()
        }
    }
}

Presentation & Interactions

Presentation Detents

Presentation detents allow you to "lock" a sheet at specific point(s).

Built-In Detents

You can use the presentationDetents on the sheet's contents to supply the sheet with presentation detents. SwiftUI provides 2 built-in detent positions

  • .medium: Stops the view about halfway up

  • .large: Similar to the normal full screen sheet style.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationDetents([.medium])
}

When you supply multiple detents to a sheet, a drag indicator is automatically visible. This helps to make it clear to users that they can drag it up or down to expand the view or dismiss.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationDetents([.medium, .large])
}

Custom Detents

There are 3 ways to create a custom detent

  1. Fraction: Setting the detent's height based on a fraction of the presentation's maximum height.

  2. Height: Setting an absolute height value.

  3. Completely Custom: You can customize it with a wide range of parameters.

Fraction-based and Height-based custom detents can be quickly created using the .fraction or .height functions.

Both built-in and custom detents can be used together. In the following example, a custom fraction and height detent is used with the built-in medium detent.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationDetents([.fraction(0.2), .medium, .height(300)])
}

Completely Custom Detents

If you prefer to have even more control over your detent positions, you can opt for a completely custom detent.

To create a completely custom detent, you will need

  • An Enumeration / Struct that conforms to the CustomPresentationDetent protocol

  • The height(in context: Context) -> CGFloat? static function within the Enumeration / Struct

  • To use the .custom(…) detent initializer to create it.

In the following example, the detents are set to halfway only in dark mode.

  • The HalfDarkModeDetent enum is created to host the height static function

  • The height static function allows you to write any logic needed to find out the presentation height

  • You can access information about the user's context with the context property

enum HalfDarkModeDetent: CustomPresentationDetent {
    static func height(in context: Context) -> CGFloat? {
        if context.colorScheme == .dark {
            return context.maxDetentValue * 0.5
        } else {
            return context.maxDetentValue
        }
    }
}

To use this as part of a detent, you can pass use the .custom value as your detent with the type of the Enumeration / Struct you created.

.sheet(isPresented: $isSheetPresented) {
    Text("Half sheets for dark mode")
        .presentationDetents([.custom(HalfDarkModeDetent.self)])
}

Disabling Interactive Dismiss

You can disable interactive dismiss of a sheet using the interactiveDismissDisabled modifier. In order to dismiss the view, you will have to use the Dismiss environment value or set the condition to present to false or nil

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .interactiveDismissDisabled()
}

Allowing Interaction in the Background View

By default, when a sheet is presented, background views will not receive user interaction. You can customize this behavior using the presentationBackgroundInteraction modifier on the sheet's contents.

In the following example, the presentation background interaction modifier is used in conjunction with the interactive dismiss disabled modifier to create a permanent on-screen search field. Since the sheet will always be presented, with no intentions for it to be dismissed, instead of supplying a Boolean Binding variable, a .constant Binding value is used.

struct ContentView: View {
    var body: some View {
        Map()
            .sheet(isPresented: .constant(true)) {
                SearchView()
                    .presentationDetents([.height(100), .large])
                    .presentationBackgroundInteraction(.enabled)
                    .interactiveDismissDisabled()
            }
    }
}

By default, when using the presentation background interaction modifier with the value set to enabled, background interaction will be disabled when the sheet is at its .large presentation detent.

You can customize this behavior with the .enabled(upThrough:) value. This allows you to supply a presentation detent where background interaction is enabled up through.

In the example below, with the upThrough value set to .large, interaction will be allowed even in the large presentation detent.

.sheet(isPresented: .constant(true)) {
    SearchView()
        .presentationDetents([.height(100), .large])
        .presentationBackgroundInteraction(.enabled(upThrough: .large))
        .interactiveDismissDisabled()
}

Configuring Scrolling / Resizing Behavior

When displaying a sheet with detents, if the sheet's contents contains a Scroll View, by default, the system will prioritize resizing the view before scrolling the contents.

If you prefer the view to prioritize scrolling before resizing, you can use the presentationContentInteraction modifier.

In the following example, a long scrolling List is created with a the presentation content interaction set to scroll. This allows the system to prioritize scrolling to the end of the List before resizing the view.

.sheet(isPresented: $isSheetPresented) {
    List(0..<50) { index in
        Text("Hello \(index)")
    }
    .presentationDetents([.medium, .large])
    .presentationContentInteraction(.scrolls)
}

Presentation content interaction can also be set to resizes. When set to resizes, the sheet will prioritize resizing its contents before scrolling the contents. This is the default configuration.

.sheet(isPresented: $isSheetPresented) {
    List(0..<50) { index in
        Text("Hello \(index)")
    }
    .presentationDetents([.medium, .large])
    .presentationContentInteraction(.resizes)
}

Presentation Appearance

SwiftUI provides several different modifiers and methods to customize the appearance of a sheet.

Customizing Presentation Background

The presentation background refers to the sheet's content background. You can use a shape style or a custom view to set the background.

Providing a Shape Style

By default, sheets use the .thickMaterial shape style. This is a translucent material. However, based on your app's context, you may want to customize this appearance.

In the following example, the thick material background is switched for a thin material. This provides a more transparent background.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationBackground(.ultraThinMaterial)
}

As the modifier accepts a Shape Style, you can also supply it with a color or gradient to set as the presentation background color.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationBackground(.green)
}
.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationBackground(.red.opacity(0.5))
}
.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationBackground(.linearGradient(colors: [.yellow, .orange], 
                                                startPoint: .topLeading, 
                                                endPoint: .bottomTrailing))
}

Providing a Custom View

For even more customization, you can supply a custom SwiftUI view as the background.

In the following example, a custom SheetBackgroundView is created and passed in.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationBackground {
            SheetBackgroundView()
        }
}
struct SheetBackgroundView: View {
    var body: some View {
        VStack {
            HStack {
                Image(systemName: "hand.wave.fill")
                    .rotationEffect(.degrees(-20))
                Spacer()
                Image(systemName: "platter.filled.bottom.iphone")
                    .rotationEffect(.degrees(15))
            }
            Spacer()
            HStack {
                Image(systemName: "platter.filled.bottom.and.arrow.down.iphone")
                    .rotationEffect(.degrees(-15))
                Spacer()
                Image(systemName: "xmark.circle.fill")
                    .rotationEffect(.degrees(10))
            }
        }
        .font(.system(size: 200))
        .foregroundStyle(.blue.opacity(0.5))
        .background(.thickMaterial)
    }
}

Corner Radius

You can customize the sheet's corner radius by using the presentationCornerRadius modifier and supplying a corner radius amount.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationCornerRadius(64)
}

Drag Indicator Visibility

You can customize the drag indicator's visibility with the presentationDragIndicator modifier.

By default,

  • If the sheet contains multiple detents, the drag indicator is automatically visible

  • If the sheet has no detents specified or just one, the drag indicator is hidden

In the following example, the drag indicator can be hidden from a sheet with detents. The detents will still continue to work as intended.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationDetents([.medium, .large])
        .presentationDragIndicator(.hidden)
}

This can also be used to add the indicator to a context where it would otherwise be hidden, such as when there are no presentation detents specified or only one presentation detent.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationDragIndicator(.visible)
}

Sizing

Compact Adaptation

By default, on iPhone, sheets adapt based on the orientation.

  • When portrait, the sheet appears as the traditional sheet.

  • When landscape, the sheet appears as a full screen cover.

This behavior can be customized with the presentationCompactAdaptation modifier.

In the following example, by setting the presentation compact adaptation to none, the sheet appears as a sheet when in landscape on iPhone.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationCompactAdaptation(.none)
}

Presentation Sizing

You can customize the presentation sizing. There are 3 main options

  • Page: roughly the size of a page of paper, designed for informational content

  • Form: slightly smaller than page, designed for forms. The default sizing option.

  • Fitted: Fits the presentation size to the contents

Page

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationSizing(.page)
}

Form

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationSizing(.form)
}

Fitted

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationSizing(.fitted)
}

Making Page/Form Fitted

You can make Page or Form fitted in a specific axis by using the .fitted function.

The .fitted function accepts two parameters, horizontal and vertical. This allows you to specify which axis the sheet should fit to.

In the following example, you can create a long sheet by fitting it to only the horizontal axis.

.sheet(isPresented: $isSheetPresented) {
    Text("Hello from the sheet!")
        .presentationSizing(.page.fitted(horizontal: true, vertical: false))
}

© 2024 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.

© 2024 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.

© 2024 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.