SheetKit: SwiftUI Modal View Extension Library

Published on

Get weekly handpicked updates on Swift and SwiftUI!

What is SheetKit?

SheetKit is an extension library for SwiftUI modal views. It provides several convenient methods for presenting and dismissing modal views, as well as a few View Extensions for modal views.

The main reasons for developing SheetKit are:

  • Facilitating Deep Link Calls

    SwiftUI offers the onOpenURL method, allowing apps to easily respond to Deep Links. However, in practice, things don’t always go as expected. This is mainly because important SwiftUI view presentation modes like NavigationView and Sheet lack quick and easy reset capabilities. It’s hard to set the app to the desired view state with just a couple of lines of code.

  • Central Management of Modal Views

    SwiftUI typically uses .sheet to create modal views. This approach is very intuitive for simple applications, but it can become chaotic and disorganized for more complex apps with numerous modal views. In such cases, it’s common to manage all modal views centrally for unified calling. For more information, refer to my previous article - Pop up different Sheets in SwiftUI as needed.

  • New Half-Height Modal View

    At WWDC 2021, Apple introduced the much-anticipated half-height modal view. Perhaps because it was introduced in haste, this popular interaction mode did not come with a SwiftUI version and only supported UIKit. SheetKit temporarily fills this gap. Whether it’s a sheet, fullScreenCover, or bottomSheet (half-height modal view), all are fully supported and managed uniformly.

System Requirements

  • iOS 15
  • Swift 5.5
  • XCode 13.0 +

By stripping out modal view support, SheetKit can also support iOS 14.

Installation

SheetKit supports installation via SPM.

Source Address

Each feature in SheetKit is concentrated in one or two files. If you only need some of the features, adding the corresponding files directly to your project might be a good choice.

Detailed Features of SheetKit

present

SheetKit Invocation

image-20210916185555507

Using SheetKit in code is very easy. It supports two methods: using SheetKit’s instance directly or using the environment value in the view. For example, both of the following code snippets will display a standard Sheet:

Swift
Button("show sheet"){
   SheetKit().present{
     Text("Hello world")
   }
}

Or

Swift
@Environment(\.sheetKit) var sheetKit

Button("show sheet"){
   sheetKit.present{
     Text("Hello world")
   }
}

SheetKit supports multi-level Sheet presentations. The following code will display two layers of Sheets:

Swift
@Environment(\.sheetKit) var sheetKit

Button("show sheet"){
   sheetKit.present{
     Button("show full sheet"){
       sheetKit.present(with:.fullScreenCover){
         Text("Hello world")
       }
     }
   }
}

Animation

In SheetKit, the animations for both present and dismiss can be disabled (especially suitable for Deep link scenarios). Use the following statement to disable the show animation:

Swift
SheetKit().present(animated: false)

Sheet Types

Currently, SheetKit supports three types of modal views: sheet, fullScreenCover, and bottomSheet.

image-20210916190606032

The following code will display a preset bottomSheet view:

Swift
sheetKit.present(with: .bottomSheet){
  Text("Hello world")
}

bottomSheet1

The bottomSheet can be customized.

image-20210916190453672

The following code will create a custom bottomSheet:

Swift
let configuration = SheetKit.BottomSheetConfiguration(  detents: [.medium(), .large()],
                                                        largestUndimmedDetentIdentifier: .medium,
                                                        prefersGrabberVisible: true,
                                                        prefersScrollingExpandsWhenScrolledToEdge: false,
                                                        prefersEdgeAttachedInCompactHeight: false,
                                                        widthFollowsPreferredContentSizeWhenEdgeAttached: true,
                                                        preferredCornerRadius: 100)

sheetKit.present(with: .customBottomSheet, configuration: configuration) {
  Text("Hello world")
}

Simulator Screen Shot - iPhone 13 Pro Max - 2021-09-16 at 16.15.08

When the bottomSheet changes height, there are two ways to get notified.

Method 1:

Swift
@State var detent: UISheetPresentationController.Detent.Identifier = .medium

Button("Show"){
  sheetKit.present(with: .bottomSheet, detentIdentifier: $detent){
    Text("Hello world")
  }
}
.onChange(of: detent){ value in
    print(value)
}

Method 2:

Swift
@State var publisher = NotificationCenter.default.publisher(for: .bottomSheetDetentIdentifierDidChanged, object: nil)

.onReceive(publisher){ notification in
       guard let obj = notification.object else {return}
       print(obj)
}

When using Method 2, if you need to display multiple layers of bottomSheet, please define different Notification Names for different levels of views.

dismissAllSheets

image-20210916190651604

SheetKit supports quickly dismissing all currently displayed modal views (regardless of whether they were presented by SheetKit). Use the following code:

Swift
SheetKit().dismissAllSheets()

Supports animation control and onDisappear:

Swift
    SheetKit().dismissAllSheets(animated: false, completion: {
        print("sheet has dismiss")
    })

dismiss

If you only want to dismiss the topmost modal view, you can use dismiss:

Swift
    SheetKit().dismiss()

Also supports animation control:

If you execute SheetKit methods outside of a view, make sure the code runs on the main thread. You can use forms like DispatchQueue.main.async or MainActor.run, etc.

interactiveDismissDisabled

An enhanced version of SwiftUI 3.0’s interactiveDismissDisabled, this feature not only allows control over whether to permit gesture-based dismissal through code, but also provides notifications when the user attempts to dismiss using a gesture.

For more information, refer to How to implement interactiveDismissDisabled in SwiftUI.

The interactiveDismissDisabled in SheetKit has been modified for compatibility with bottomSheet. For specific changes, please refer to the source code.

Swift
struct ContentView: View {
    @State var sheet = false
    var body: some View {
        VStack {
            Button("show sheet") {
                sheet.toggle()
            }
        }
        .sheet(isPresented: $sheet) {
            SheetView()
        }
    }
}

struct SheetView: View {
    @State var disable = false
    @State var attempToDismiss = UUID()
    var body: some View {
        VStack {
            Button("disable: \(disable ? "true" : "false")") {
                disable.toggle()
            }
            .interactiveDismissDisabled(disable, attempToDismiss: $attempToDismiss)
        }
        .onChange(of: attempToDismiss) { _ in
            print("try to dismiss sheet")
        }
    }
}

dismissSheet

clearBackground

Sets the modal view’s background to transparent. In SwiftUI 3.0, it’s already possible to create various frosted glass effects using native APIs. However, these effects only become visible when the background of the modal view is set to transparent.

In the modal view:

Swift
.clearBackground()

For example:

Swift
        ZStack {
            Rectangle().fill(LinearGradient(colors: [.red, .green, .pink, .blue, .yellow, .cyan, .gray], startPoint: .topLeading, endPoint: .bottomTrailing))
            Button("Show bottomSheet") {
                sheetKit.present(with: .bottomSheet, afterPresent: { print("presented") }, onDisappear: { print("disappear") }, detentIdentifier: $detent) {
                    ZStack {
                        Rectangle()
                            .fill(.ultraThinMaterial)
                        VStack {
                            Text("Hello world")
                            Button("dismiss all") {
                                SheetKit().dismissAllSheets(animated: true, completion: {
                                    print("sheet has dismiss")
                                })
                            }
                        }
                    }
                    .clearBackground()
                    .ignoresSafeArea()
                }
            }
            .foregroundColor(.white)
            .buttonStyle(.bordered)
            .controlSize(.large)
            .tint(.green)
        }
        .ignoresSafeArea()

Simulator Screen Shot - iPhone 13 Pro Max - 2021-09-16 at 19.19.34

Conclusion

Both SheetKit and NavigationViewKit are libraries I developed for the new version of Health Notes. The functionalities are primarily based on my personal needs. If there are any other feature requests, please let me know through Twitter, blog comments, or Issues.

I'm really looking forward to hearing your thoughts! Please Leave Your Comments Below to share your views and insights.

Fatbobman(东坡肘子)

I'm passionate about life and sharing knowledge. My blog focuses on Swift, SwiftUI, Core Data, and Swift Data. Follow my social media for the latest updates.

You can support me in the following ways