Sheet in SwiftUI (iOS 15+)

DevTechie Inc
May 11, 2023

SwiftUI sheet modifier is used when we want to present a modal view to the user when a provided boolean value becomes true.

In this article, we will explore the sheet modifier.

Let’s create an example to work with.

struct DevTechieSheetExample: View {
    var body: some View {
        VStack {
            Text("DevTechie")
                .font(.largeTitle)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(.orange)
            
            Spacer()
            
            Text("Press the button to launch a sheet.")
            
            Button("Tap me") {
                
            }
            
            Spacer()
        }
    }
}

In order to add a sheet, we need a boolean variable which we can pass as binding value to isPresented parameter of sheet modifier. We will add a State property called showSheet.

@State private var showSheet = false

Next, we will add sheet modifier to the Button and toggle the showSheet State variable. As the content trailing closure for the sheet modifier, we will simply create a Text view.

struct DevTechieSheetExample: View {
    @State private var showSheet = false
    
    var body: some View {
        VStack {
            Text("DevTechie")
                .font(.largeTitle)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(.orange)
            
            Spacer()
            
            Text("Press the button to launch a sheet.")
            
            Button("Tap me") {
                showSheet.toggle()
            }
            .sheet(isPresented: $showSheet) {
                Text("This is the sheet view.")
                    .font(.largeTitle)
            }
            
            Spacer()
        }
    }
}

Notice that SwiftUI presents entire view even though we passed just Text view. SwiftUI also adds swipe down to dismiss the view functionality to the presented sheet.

We can build a separate view and pass that as the content for Sheet view.

struct SheetView: View {
    var body: some View {
        VStack {
            Text("This is a sheet view.")
                .font(.title)
                .padding()
                .frame(maxWidth: .infinity)
                .background(.orange)
                
            Spacer()
            Text("DevTechie")
                .font(.largeTitle)
            
            Spacer()
        }
    }
}

We can programmatically dismiss the sheet using dismiss environmentvariable.

@Environment(\.dismiss) var dismiss

Let’s add a close button to our sheet view and dismiss the view by calling dismiss function from the sheet view.

struct SheetView: View {
    
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            HStack {
                Text("This is a sheet view.")
                    .font(.title)
                Spacer()
                Button {
                    dismiss()
                } label: {
                    Image(systemName: "xmark.circle")
                        .font(.title)
                }
            }
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(.orange)
            
            Spacer()
            Text("DevTechie")
                .font(.largeTitle)
            
            
            Spacer()
        }
    }
}

Starting iOS 15, we can disable swipe to dismiss functionality from sheetwith the help of interactiveDismissDisabled modifier. This is helpful if we are expecting users to take action on the sheet view before they close the sheet down. An example for this could be license or terms agreement view.

Let’s add this modifier to our sheet view.

struct SheetView: View {
    
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            HStack {
                Text("This is a sheet view.")
                    .font(.title)
                Spacer()
                Button {
                    dismiss()
                } label: {
                    Image(systemName: "xmark.circle")
                        .font(.title)
                }
            }
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(.orange)
            
            Spacer()
            Text("DevTechie")
                .font(.largeTitle)
            
            
            Spacer()
        }
        .interactiveDismissDisabled()
    }
}

interactiveDismissDisabled modifier takes boolean value as parameter as well so we can enable/disable swipe to dismiss based on a condition.

We will create a State variable in our main view. We also need to add a Binding property to SheetView. SheetView will have a Toggle so user can agree to the terms and upon the state change, we can enable swipe to dismiss functionality.

struct DevTechieSheetExample: View {
    @State private var showSheet = false
    
    @State private var agreeTerms = false
    
    var body: some View {
        VStack {
            Text("DevTechie")
                .font(.largeTitle)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(.orange)
            
            Spacer()
            
            Text("Press the button to launch a sheet.")
            
            Button("Tap me") {
                showSheet.toggle()
            }
            .sheet(isPresented: $showSheet) {
                SheetView(agreeTerms: $agreeTerms)
            }
            
            Spacer()
        }
    }
}struct SheetView: View {
    
    @Environment(\.dismiss) var dismiss
    @Binding var agreeTerms: Bool
    
    var body: some View {
        VStack {
            HStack {
                Text("This is a sheet view.")
                    .font(.title)
                Spacer()
                Button {
                    dismiss()
                } label: {
                    Image(systemName: "xmark.circle")
                        .font(.title)
                }
            }
            .foregroundColor(.white)
            .padding()
            .frame(maxWidth: .infinity)
            .background(.orange)
            
            Spacer()
            Text("DevTechie")
                .font(.largeTitle)
            Toggle("Agree to our terms", isOn: $agreeTerms)
                .padding()
            
            Spacer()
        }
        .interactiveDismissDisabled(!agreeTerms)
    }
}

Notice that our terms boolean value is preserved, what if we want to reset the value when user dismisses the sheet. We can use another overload which takes onDismiss function. onDismiss function executes when sheet is dismissed.

struct DevTechieSheetExample: View {
    @State private var showSheet = false
    
    @State private var agreeTerms = false
    
    var body: some View {
        VStack {
            Text("DevTechie")
                .font(.largeTitle)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(.orange)
            
            Spacer()
            
            Text("Press the button to launch a sheet.")
            
            Button("Tap me") {
                showSheet.toggle()
            }
            .sheet(isPresented: $showSheet, onDismiss: onDismiss) {
                SheetView(agreeTerms: $agreeTerms)
            }
            
            Spacer()
        }
    }
    
    func onDismiss() {
        agreeTerms = false
    }
}