Sheets in SwiftUI 📄

Dec 26, 2020 3 min read
Sheets in SwiftUI 📄

SwiftUI’s sheets are used to present new view controllers modally over existing ones. Let's cover how to implement it simply and concise.

SwiftUI’s sheets are used to present new view controllers modally over existing ones. To use one, give it something to show (some text, an image, a custom view, etc), add a Boolean that defines whether the detail view should be showing, then attach it to your main view as a modal sheet.

For iOS 15 and above


Present a new view

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

    var body: some View {
        Button("Press to dismiss") {
            dismiss()
        }
        .font(.title)
        .padding()
        .background(Color.black)
    }
}

struct ContentView: View {
    @State private var showingSheet = false

    var body: some View {
        Button("Show Sheet") {
            showingSheet.toggle()
        }
        .sheet(isPresented: $showingSheet) {
            SheetView()
        }
    }
}


Full Screen Sheet

SwiftUI’s fullScreenCover() modifier gives us a presentation style for times when you want to cover as much of the screen as possible, and in code it works almost identically to regular sheets.

For example, this will present a full screen modal view when the button is pressed:

import SwiftUI

struct ContentView: View {
    
    @State private var isPresented = false
    
    var body: some View {
        Button("Present!") {
            self.isPresented.toggle()
        }
        .fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
    }
}

struct FullScreenModalView: View {
    // Needed to keep track that a sheet was presented
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        VStack {
            HStack {
                Button(action: {}) {
                    Text("Cancel")
                        .foregroundColor(Color.blue)
                        .padding(.leading, 30)
                        .padding(.top, 10)
                        .onTapGesture {
                            presentationMode.wrappedValue.dismiss()
                        }
                }
                Spacer()
            }
            
            Spacer()
            
            VStack {
                Text("This is a modal view")
            }
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color("Bg").ignoresSafeArea(.all, edges: .all))    // Set your custom color here, or not :D
    }
}

Half Sheet Modal

Demo

You can customize whether it goes all the way up or not in presentationController.detents, for example, just leave medium()

// Custom Half Sheet Modifier....
extension View{
    
    // Binding Show Variable...
    func halfSheet<SheetView: View>(showSheet: Binding<Bool>,@ViewBuilder sheetView: @escaping ()->SheetView,onEnd: @escaping ()->())->some View{
        
        // why we using overlay or background...
        // bcz it will automatically use the swiftui frame Size only....
        return self
            .background(
                HalfSheetHelper(sheetView: sheetView(),showSheet: showSheet, onEnd: onEnd)
            )
            .onChange(of: showSheet.wrappedValue) { newValue in
                if !newValue{
                    onEnd()
                }
            }
    }
}

// UIKit Integration...
struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable{
    
    var sheetView: SheetView
    @Binding var showSheet: Bool
    var onEnd: ()->()
    
    let controller = UIViewController()
    
    func makeCoordinator() -> Coordinator {
        
        return Coordinator(parent: self)
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
    
        controller.view.backgroundColor = .clear
        
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
     
        if showSheet{
            
            if uiViewController.presentedViewController == nil{
                
                // presenting Modal View....
                
                let sheetController = CustomHostingController(rootView: sheetView)
                sheetController.presentationController?.delegate = context.coordinator
                uiViewController.present(sheetController, animated: true)
            }
        }
        else{
            // closing view when showSheet toggled again...
            if uiViewController.presentedViewController != nil{
                uiViewController.dismiss(animated: true)
            }
        }
    }
    
    // On Dismiss...
    class Coordinator: NSObject,UISheetPresentationControllerDelegate{
        
        var parent: HalfSheetHelper
        
        init(parent: HalfSheetHelper) {
            self.parent = parent
        }
        
        func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
            parent.showSheet = false
        }
    }
}

// Custom UIHostingController for halfSheet....
class CustomHostingController<Content: View>: UIHostingController<Content>{
    
    override func viewDidLoad() {
                
        // setting presentation controller properties...
        if let presentationController = presentationController as? UISheetPresentationController{
            
            presentationController.detents = [
                
                .medium()
//                .large()
            ]
            
            // to show grab protion...
            presentationController.prefersGrabberVisible = true
        }
    }
}


// View:

struct ShowSheet: View{
    
    @State var showSheet: Bool = false
    
    var body: some View{
        Button {
            showSheet.toggle()
        } label: {
            Text("Present Sheet")
        }
        .halfSheet(showSheet: $showSheet) {
            SheetView()
        } onEnd: {
            print("Dismissed")
        }
    }
}

Prevent the dismissable of forms and sheets

There’s an interactiveDismissDisabled function where the developer can choose to prevent the dismissable of forms and sheets. Here’s a quick illustration:

Great! Next, complete checkout for full access to ArturoFM.
Welcome back! You've successfully signed in.
You've successfully subscribed to ArturoFM.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info has been updated.
Your billing was not updated.