Customizable sheets using UISheetPresentationController in SwiftUI
PageSheet
Customizable sheet presentations in SwiftUI. Using UISheetPresentationController
under the hood.
Features
- Uses the default
sheet
API under the hood, ensuring maximum compatibility & stability. - Exposes the exact same API as the default SwiftUI
sheet
implementation. - No hacks, follows the best practices for creating represetable views in SwiftUI.
- Configurable using view modifiers, can configure
UISheetPresentationController
from any child views in the presented sheet’s content view. - Works with the
interactiveDismissDisabled(_:Bool)
modifier. - Exposes all of the
UISheetPresentationController
configuration options. - Track the currently selected detent using an
Environment
value. - Well documented API, following a similar approach to the Developer Documentation.
- Small footprint, weighing only
34.6kB
when installed via SwiftPM.
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/PageSheet
and tap Next
.
Select the PageSheet
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/PageSheet.git", .upToNextMajor(from: "1.0.0"))
Next, add PageSheet
as a dependency for your targets:
.target(name: "AppTarget", dependencies: ["PageSheet"])
A completed example may look like this:
// swift-tools-version:5.5
import PackageDescription
let package = Package(
name: "App",
dependencies: [
.package(
url: "https://github.com/ericlewis/PageSheet.git",
.upToNextMajor(from: "1.0.0"))
],
targets: [
.target(
name: "AppTarget",
dependencies: ["PageSheet"])
]
)
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
PageSheet
works similarly to a typical sheet
view modifier.
import SwiftUI
import PageSheet
struct ContentView: View {
@State
private var sheetPresented = false
var body: some View {
Button("Open Sheet") {
sheetPresented = true
}
.pageSheet(isPresented: $sheetPresented) {
Text("Hello!")
}
}
}
PageSheet
also supports presentation via conditional Identifiable
objects.
import SwiftUI
import PageSheet
struct ContentView: View {
@State
private var string: String?
var body: some View {
Button("Open Sheet") {
string = "Hello!"
}
.pageSheet(item: $string) { unwrappedString in
Text(unwrappedString)
}
}
}
extension String: Identifiable {
public var id: String { self }
}
Customization
PageSheet
can also be customized using a collection of view modifiers applied to the sheet’s content.
import SwiftUI
import PageSheet
struct ContentView: View {
@State
private var sheetPresented = false
var body: some View {
Button("Open Sheet") {
sheetPresented = true
}
.pageSheet(isPresented: $sheetPresented) {
Text("Hello!")
.preferGrabberVisible(true)
}
}
}
Documentation
Presentation Modifiers
These modifiers behave exactly the same way as the sheet
presentation modifiers in SwiftUI.
func pageSheet<Content: View>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, content: @escaping () -> Content) -> some View
Presents a configurable page sheet when a binding to a Boolean value that you provide is true.
Use this method when you want to present a configurable sheet view to the user when a Boolean value 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.
func pageSheet<Item: Identifiable, Content: View>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping () -> Content) -> some View
Presents a sheet using the given item as a data source for the sheet’s content.
Use this method when you need to present a customizable sheet view with content from a custom data source.
Parameters:
item
: A binding to an optional source of truth for the sheet.
Whenitem
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. Ifitem
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.
Presentation Customization Modifiers
These modifiers only take effect when the modified view is inside of and visible within a presented PageSheet
.
Note: You can apply these modifiers to any view in the sheet’s view hierarchy.
func preferGrabberVisible(_ isVisible: Bool) -> some View
Sets a Boolean value that determines whether the presenting sheet shows a grabber at the top.
The default value is
false
, which means the sheet doesn’t show a grabber. A grabber is a visual affordance that indicates that a sheet is resizable.
Showing a grabber may be useful when it isn’t apparent that a sheet can resize or when the sheet can’t dismiss interactively.Set this value to
true
for the system to draw a grabber in the standard system-defined location.
The system automatically hides the grabber at appropriate times, like when the sheet is full screen in a compact-height size class or when another sheet presents on top of it.
Parameters:
isVisible
: Default value isfalse
, set totrue
to display grabber.
Returns:
- A view that wraps this view and sets the presenting sheet’s grabber visiblity.
func detents(_ detents: PageSheet.Detents) -> some View
Sets an array of heights where the presenting sheet can rest.
The default value is an array that contains the value
large()
.
The array must contain at least one element. When you set this value, specify detents in order from smallest to largest height.
Parameters:
detents
: The default value is an array that contains the valuelarge()
.
Returns:
- A view that wraps this view and sets the presenting sheet’s
UISheetPresentationController/detents
.
func largestUndimmedDetent(id identifier: PageSheet.Detent.Identifier?) -> some View
Sets the largest detent that doesn’t dim the view underneath the presenting sheet.
The default value is
nil
, which means the system adds a noninteractive dimming view underneath the sheet at all detents.
Set this property to only add the dimming view at detents larger than the detent you specify.
For example, set this property tomedium
to add the dimming view at thelarge
detent.Without a dimming view, the undimmed area around the sheet responds to user interaction, allowing for a nonmodal experience.
You can use this behavior for sheets with interactive content underneath them.
Parameters:
id
: An optionalPageSheet.Detent.Identifier
value, the default isnil
.
Returns:
- A view that wraps this view and sets the presenting sheet’s largest undimmed
Detent
identifier.
func selectedDetent(id identifier: PageSheet.Detent.Identifier?) -> some View
Sets the identifier of the most recently selected detent on the presenting sheet.
This property represents the most recent detent that the user selects or that you set programmatically.
The default value isnil
, which means the sheet displays at the smallest detent you specify indetents
.
Parameters:
id
: An optionalPageSheet.Detent.Identifier
value, the default isnil
.
Returns:
- A view that wraps this view and sets the presenting sheet’s selected
Detent
identifier.
func preferEdgeAttachedInCompactHeight(_ preference: Bool) -> some View
Sets a Boolean value that determines whether the presenting sheet attaches to the bottom edge of the screen in a compact-height size class.
The default value is
false
, which means the sheet defaults to a full screen appearance at compact height.
Set this value totrue
to use an alternate appearance in a compact-height size class, causing the sheet to only attach to the screen on its bottom edge.
Parameters:
preference
: Default value isfalse
.
Returns:
- A view that wraps this view and sets the presenting sheet’s
prefersEdgeAttachedInCompactHeight
property.
func widthFollowsPreferredContentSizeWhenEdgeAttached(_ preference: Bool) -> some View
Sets a Boolean value that determines whether the presenting sheet’s width matches its view’s preferred content size.
The default value is
false
, which means the sheet’s width equals the width of its container’s safe area.
Set this value totrue
to use your view controller’spreferredContentSize
to determine the width of the sheet instead.This property doesn’t have an effect when the sheet is in a compact-width and regular-height size class, or when
prefersEdgeAttachedInCompactHeight
isfalse
.
Parameters:
preference
: Default value isfalse
.
Returns:
- A view that wraps this view and sets the presenting sheet’s
prefersEdgeAttachedInCompactHeight
property.
func preferScrollingExpandsWhenScrolledToEdge(_ preference: Bool) -> some View
Sets a Boolean value that determines whether scrolling expands the presenting sheet to a larger detent.
The default value is
true
, which means if the sheet can expand to a larger detent thanselectedDetentIdentifier
,
scrolling up in the sheet increases its detent instead of scrolling the sheet’s content. After the sheet reaches its largest detent, scrolling begins.Set this value to
false
if you want to avoid letting a scroll gesture expand the sheet.
For example, you can set this value on a nonmodal sheet to avoid obscuring the content underneath the sheet.
Parameters:
preference
: Default value istrue
.
Returns:
- A view that wraps this view and sets the presenting sheet’s
prefersScrollingExpandsWhenScrolledToEdge
property.
func preferredSheetCornerRadius(CGFloat?) -> some View
Sets the preferred corner radius on the presenting sheet.
The default value is
nil
. This property only has an effect when the presenting sheet is at the front of its sheet stack.
Parameters:
preference
: Default value isnil
.
Returns:
- A view that wraps this view and sets the presenting sheet’s
cornerRadius
.
func preferredSheetCornerRadius(_ cornerRadius: CGFloat?) -> some View
Known Issues
- Largest undimmed detent seems to affect the dimming of accent color elements in parent views.
- The
selectedDetentIdentifier
value inEnvironment
may not update if the selectedDetent
identifier is changed programmatically.
License
PageSheet is released under the MIT license. See LICENSE for details.