SwiftUI Sheet Explained: Modal Views for iOS Apps

SwiftUI Sheet Explained: Modal Views for iOS Apps

iOSSwiftSwitUI
Fix bugs faster! Log Collection Made Easy
START NOW!

In this article we’ll learn about Sheets in SwiftUI. We will explain when, and how, to use them, we’ll explain how they’re created and shown. We’ll also look at a few examples of their usage. Let’s get started!

What are SwiftUI Sheets and when should we use them?

In SwiftUI, Sheets are views that are modally shown on top of the current content on the screen, without the need to navigate to a new page. They are useful for showing content that is contextually relevant to what the user is currently seeing.

“When should we use them?” is a great question that we’ll answer with our own view of it. When the content we want to show is too complex for a simple Alert, and not complex enough to show an entire screen or flow, that’s a great place to use contextual Sheets. Examples of that would be product detail pages, views to add items to collections, screens to edit the details of items, and so on.

How to use Sheets

Now let’s get our hands into the dough and look at how Sheets work from a technical perspective.

Showing and hiding a SwiftUI Sheet

Let’s start by looking at how a SwiftUI Sheet is shown. This is simple, since showing a Sheet is just a matter of adding a modifier to our current View:

.sheet(isPresented: $myBooleanCondition) {
    ViewToShow()
}

That is the simplest possible way to show a Sheet. isPresented takes a Boolean and shows the Sheet when the condition is true.

Now let’s have a more complete example that is made of a Button that shows a Sheet which is dismissible:

struct ContentView: View {
    @State private var isShown: Bool = false
    
    var body: some View {
        Button {
            isShown = true
        } label: {
            Text("Show the Sheet")
        }.sheet(isPresented: $isShown){
            MySheet()
        }

    }
}

struct MySheet: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Button {
            dismiss()
        } label: {
            Text("Dismiss the Sheet")
        }
    }
}

Our ContentView is quite straightforward. We have a Bool isShown, that is then observed by the Sheet to know when to show our MySheet modal, and that Bool is toggled to true with the click of a button.

Now our MySheet introduces something new with @Environment(\.dismiss) var dismiss. What this single line does, is give us access to the dismiss environmental value, which then tells our SwiftUI framework to dismiss the current View. When the framework does that, the Sheet being presented knows it needs to automatically toggle the isShown to false, and that’s why it gets dismissed and can be shown again.

SwiftUI Sheet Properties

Now that we’ve looked at basic showing and dismissing of Sheets, let us dive into the properties that we can change to better customise our Sheets, and the experience they provide. They are not massively configurable, since a Sheet is just the way we present a SwiftUI View, however there’s a couple things we can change:

Enabling/Disabling drag to dismiss

If you try out the previous Sheet, you’ll see that you can dismiss the modal by dragging it downwards, which is the default behaviour. To change this we need to modify our shown View to add interactiveDismissDisabled() to it. So, the example Sheet we used previously would now be:

struct MySheet: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Button {
            dismiss()
        } label: {
            Text("Dismiss the Sheet")
        }
        .interactiveDismissDisabled()
    }
}

Full Screen covering

If you’d like your sheets to cover the entirety of the screen, instead of just the majority of it, becoming a modal view, you would have to show them using fullScreenCover. Which means that the equivalent of our:

.sheet(isPresented: $isShown){
   MySheet()
 }

would be:

.fullScreenCover(isPresented: $isShown){
    MySheet()
 }

With this change, you can start presenting sheets that become a modal sheet because they have a screen cover that prevents the user from interacting with any other child view.

Showing Several Sheets on the same view

It’s common to have support for multiple different Sheets on the same screen. There’s two main approaches to doing this. One is by having multiple Sheet entries, and the other is by having a single Sheet entry, but with the ability to identify them. We will now look into how those approaches can be used in practice.

Multiple Sheet entries

As the name implies, this method is used by having several Sheet modifiers on our Views. Each modifier should have their own boolean checks. Let’s have a look at a View with four different Sheet modifiers and four different Views that can be modally shown:

struct ContentView: View {
    @State private var isShowingSheet1: Bool = false
    @State private var isShowingSheet2: Bool = false
    @State private var isShowingSheet3: Bool = false
    @State private var isShowingSheet4: Bool = false
    
    var body: some View {
        Button {
            isShowingSheet1 = true
        } label: {
            Text("Show Sheet 1")
        }
        Button {
            isShowingSheet2 = true
        } label: {
            Text("Show Sheet 2")
        }
        Button {
            isShowingSheet3 = true
        } label: {
            Text("Show Sheet 3")
        }
        Button {
            isShowingSheet4 = true
        } label: {
            Text("Show Sheet 4")
        }
        
        .sheet(isPresented: $isShowingSheet1) {
            MySheet()
        }.sheet(isPresented: $isShowingSheet2) {
            MySecondSheet()
        }.sheet(isPresented: $isShowingSheet3) {
            MyThirdSheet()
        }.sheet(isPresented: $isShowingSheet4) {
            MyFourthSheet()
        }

    }
}

struct MySheet: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Text("I am Sheet 1")

        Button {
            dismiss()
        } label: {
            Text("Dismiss the Sheet")
        }
    }
}

struct MySecondSheet: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Text("I am Sheet 2")

        Button {
            dismiss()
        } label: {
            Text("Dismiss the Sheet")
        }
    }
}

struct MyThirdSheet: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Text("I am Sheet 3")

        Button {
            dismiss()
        } label: {
            Text("Dismiss the Sheet")
        }
    }
}

struct MyFourthSheet: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        Text("I am Sheet 4")

        Button {
            dismiss()
        } label: {
            Text("Dismiss the Sheet")
        }
    }
}

In this example we’ll have four buttons in our main View, and depending on which you choose, the respective Sheet is shown. While it does work, for screens with multiple Sheets this becomes quite wordy. We can solve that by re-writing our Sheet modifiers into a single one.

Having multiple Sheet support with a Single Sheet entry

This is usually achieved by using Enums. If you’d like to learn more about Enums, we have an article covering them, here. With the help of Enums we’ll declare a group of values that cover all our Sheets:

    enum SheetType: String, Identifiable {
        var id: String { rawValue }
        
        case sheet1, sheet2, sheet3, sheet4
    }

With that we can now add a property to our View that is of SheetType

    @State private var shownSheet: SheetType?

To show this particular Sheet we will use a different Sheet initialisation method, instead of a boolean that is observed to see when the Sheet should be shown. This initialiser takes an optional property and when there’s a value it is used to show the Sheet. It is easier seen than described, so:

        .sheet(item: $shownSheet) { sheet in
            switch sheet {
            case .sheet1: MySheet()
            case .sheet2: MySecondSheet()
            case .sheet3: MyThirdSheet()
            case .sheet4: MyFourthSheet()
            }
        }

Whenever shownSheet is not nil, the block gets called with the most up-to-date value in sheet, and we use that to show the appropriate screen.

With all that said and done, our entire View now looks like this:

 struct ContentView: View {
    enum SheetType: String, Identifiable {
        var id: String { rawValue }
        
        case sheet1, sheet2, sheet3, sheet4
    }
    
    @State private var shownSheet: SheetType?
    
    var body: some View {
        Button {
            shownSheet = .sheet1
        } label: {
            Text("Show Sheet 1")
        }
        Button {
            shownSheet = .sheet2
        } label: {
            Text("Show Sheet 2")
        }
        Button {
            shownSheet = .sheet3
        } label: {
            Text("Show Sheet 3")
        }
        Button {
            shownSheet = .sheet4
        } label: {
            Text("Show Sheet 4")
        }
        
        .sheet(item: $shownSheet) { sheet in
            switch sheet {
            case .sheet1:
                MySheet()
            case .sheet2: MySecondSheet()
            case .sheet3: MyThirdSheet()
            case .sheet4: MyFourthSheet()
            }
        }
    }
}

To Sum up

SwiftUI Sheets are a great way to show our Views modally. This helps to prevent the user from losing the context of what they were doing.

We can show truly modal sheets that do not take the entire screen by using the .sheet modifier, and full screen sheets by using the .fullScreenCover modifier.

The majority of times we can simply control their visibility by using a Boolean to toggle if it is being presented or not; we also have a secondary way of controlling that with an item-based initialiser.

We hope this article was helpful in giving you an overview of what Sheets are, and how you can use them in your own apps.

Expect the Unexpected! Debug Faster with Bugfender
START FOR FREE

Trusted By

/assets/images/svg/customers/projects/taxify.svg/assets/images/svg/customers/cool/ubiquiti.svg/assets/images/svg/customers/projects/sk_telecom.svg/assets/images/svg/customers/highprofile/credito_agricola.svg/assets/images/svg/customers/highprofile/dolby.svg/assets/images/svg/customers/cool/napster.svg/assets/images/svg/customers/highprofile/intel.svg/assets/images/svg/customers/highprofile/gls.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required