BottomSheetView


Present sheets with UISheetPresentationController in SwiftUI.

Table of Contents

Requirements

The codebase supports iOS and requires Xcode 12.0 or newer

Installation

Xcode

Open your project. Navigate to File > Swift Packages > Add Package Dependency. Enter the url https://github.com/ericlewis/BottomSheetView and tap Next.
Select the BottomSheetView target and press Add Package.

Swift Package Manager

Add the following line to the dependencies in your Package.swift file:

.package(url: "https://github.com/ericlewis/BottomSheetView.git", .upToNextMajor(from: "0.2.0"))

Next, add BottomSheetView as a dependency for your targets:

.target(name: "AppTarget", dependencies: ["BottomSheetView"])

A completed example may look like this:

// swift-tools-version:5.5

import PackageDescription

let package = Package(
    name: "ExampleApp",
    dependencies: [
        .package(
          url: "https://github.com/ericlewis/BottomSheetView.git", 
          .upToNextMajor(from: "0.2.0"))
    ],
    targets: [
        .target(
          name: "ExampleAppTarget", 
          dependencies: ["BottomSheetView"])
    ]
)

Examples

Example Project

If you are using Xcode 13.2.1 you can navigate to the Example folder and open the enclosed Swift App Playground to test various features (and see how they are implemented).

Presentation

BottomSheetView works similarly to a typical sheet view modifier.

import SwiftUI
import BottomSheetView

struct ContentView: View {
  @State
  private var sheetPresented = false
  
  var body: some View {
    Button("Open Sheet") {
      sheetPresented = true
    }
    .bottomSheet(isPresented: $sheetPresented) {
      Text("Hello!")
    }
  }
}

BottomSheetView also supports presentation via conditional Identifiable objects.

import SwiftUI
import BottomSheetView

struct ContentView: View {
  @State
  private var string: String?
  
  var body: some View {
    Button("Open Sheet") {
      string = "Hello!"
    }
    .bottomSheet(item: $string) { unwrappedString in
      Text(unwrappedString)
    }
  }
}

extension String: Identifiable {
  public var id: String { self }
}

Customization

BottomSheetView can also be customized using a collection of view modifiers applied to the bottom sheet’s content.

import SwiftUI
import BottomSheetView

struct ContentView: View {
  @State
  private var sheetPresented = false
  
  var body: some View {
    Button("Open Sheet") {
      sheetPresented = true
    }
    .bottomSheet(isPresented: $sheetPresented) {
      Text("Hello!")
        .prefersGrabberVisible(true) // Always applied to children, similar to navigation views.
    }
  }
}

Documentation

Environment

You can access the current selectedDetentIdentifier property via the SwiftUI Environment property wrapper.

...
@Environment(\.selectedDetentIdentifier)
var selectedDetent

Presentation

These are extensions on View.

/// Presents a sheet using `UISheetPresentationController` when a binding to a
/// Boolean value that you provide is true.
///
/// - Parameters:
///   - isPresented: A binding to a Boolean value that determines whether
///     to present the sheet that you create in the modifier's
///     `content` closure.
///   - onDismiss: The closure to execute when dismissing the sheet.
///   - content: A closure that returns the content of the sheet.
public func bottomSheet<Content: View>(
  isPresented: Binding<Bool>,
  onDismiss: (() -> Void)? = nil,
  @ViewBuilder content builder: @escaping () -> Content
) -> some View

/// Presents a sheet via `UISheetPresentationController` using the given
/// item as a data source for the sheet's content.
///
/// - Parameters:
///   - item: A binding to an optional source of truth for the sheet.
///     When `item` is non-`nil`, the system passes the item's content to
///     the modifier's closure. You display this content in a sheet that you
///     create that the system displays to the user. If `item` changes,
///     the system dismisses the sheet and replaces it with a new one
///     using the same process.
///   - onDismiss: The closure to execute when dismissing the sheet.
///   - content: A closure returning the content of the sheet.
public func bottomSheet<Item: Identifiable, Content: View>(
  item: Binding<Item?>,
  onDismiss: (() -> Void)? = nil,
  @ViewBuilder content builder: @escaping (Item) -> Content
) -> some View

Styling & Behavior

These are extensions on View, they apply preferences to BottomSheetView. The ViewModifiers themselves are not public.

/// The array of heights where a sheet can rest.
public func detents(_ detents: [UISheetPresentationController.Detent]) -> some View

/// The largest detent that doesn’t dim the view underneath the sheet.
public func largestUndimmedDetentIdentifier(_ id: UISheetPresentationController.Detent.Identifier?) -> some View 

/// A Boolean value that determines whether scrolling expands the sheet to a larger detent.
public func prefersScrollingExpandsWhenScrolledToEdge(_ preference: Bool) -> some View 

/// A Boolean value that determines whether the sheet shows a grabber at the top.
public func prefersGrabberVisible(_ preference: Bool) -> some View

/// A Boolean value that determines whether the sheet attaches to the bottom edge of the screen in a compact-height size class.
public func prefersEdgeAttachedInCompactHeight(_ preference: Bool) -> some View

/// A Boolean value that determines whether the sheet's width matches its view controller's preferred content size.
public func widthFollowsPreferredContentSizeWhenEdgeAttached(_ preference: Bool) -> some View

/// The corner radius that the sheet attempts to present with.
public func preferredCornerRadius(_ preference: CGFloat?) -> some View

/// Conditionally prevents interactive dismissal of a popover or a sheet.
public func dismissDisabled(_ preference: Bool) -> some View

Known Issues

  • Largest undimmed detent changes seem to affect the dimming of accent color elements in parent views.
  • Attempt to set a constant value for item or isPresented results in the sheet not being presented.
  • Creating a bottom sheet & doing all layout work in the bottom sheet itself results in missing accent colors.
    • Workaround: instead, create a new View and use that in .bottomSheet

License

BottomSheetView is released under the MIT license. See LICENSE for details.

GitHub

View Github