CustomPresentationDetent Protocol Implementation in SwiftUI for Modal Presentations
Build more flexible and customized modal view presentations
We can build more flexible and customized modal view presentations thanks to the CustomPresentationDetent
protocol. It's common to use the .sheet
modifier when presenting a modal view. However, occasionally we may need more control over the presentation and the .sheet
's default behavior will not be enough. This is where CustomPresentationDetent
implementation begins because it enables us to change the presentation's behavior in the modal view.
What you will learn
The article provides and outlines a step-by-step guide on how to use CustomPresentationDetent
. You will download a starter project that explains how to create an extension of PresentationDetent
struct and implements CustomPresentationDetent
protocol.
Getting started with the starter project
Create
PresentationDetent
extensionImplementing
CustomPresentationDetent
Getting started with the starter project
Before we start, clone/fork or download the project from the main branch
and run the following git
commands:
git clone git@github.com:amarildolucas/CustomPresentationDetent.git
cd CustomPresentationDetent
git checkout start-project
The
main
branch contains the final result of the implementation.The
start-project
branch contains the version that you should use to follow through with this article. It is highly recommended that you use thestart-project
branch.
The start project contains a ContentView.swift
file that:
Defines a SwiftUI view.
Contains a button that, when pressed, toggles the boolean value of
isPresentingSheet
and presents a modal sheet using the.sheet
modifier. The content of the modal sheet is defined in a separate struct calledContentModalSheet
. TheContentView
struct also conforms to the View protocol and has a body property that returns aVStack
containing the button.
// ContentView.swift
struct ContentView {
// Used to toggle and present the modal sheet.
@State private var isPresentingSheet: Bool = false
}
extension ContentView: View {
// Returns a VStack containing the button.
var body: some View {
VStack {
Button(action: presentSheet) {
HStack {
Image(systemName: "arrow.up")
Text("Present Sheet")
}
.fontWeight(.semibold)
.foregroundColor(.white)
.padding()
.background(Color.accentColor)
.cornerRadius(8)
}
.sheet(
isPresented: $isPresentingSheet,
content: ContentModalSheet.init
)
}
.padding()
}
}
extension ContentView {
// Toggles the boolean value of isPresentingSheet and presents a modal sheet using the .sheet modifier.
private func presentSheet() {
isPresentingSheet.toggle()
}
}
The start project also contains another file called ContentModalSheet.swift
that contains the View to be presented.
Defines a view called
ContentModalSheet
, which is presented as a modal sheet.Display the
Text
"Showing Modal Sheet" and theButton
that dismisses the sheet by using the Environmentdismiss
property and calling the.callAsFunction
method.
// ContentModalSheet.swift
struct ContentModalSheet {
@Environment(\\.dismiss) private var dismissModalSheet
}
extension ContentModalSheet: View {
var body: some View {
VStack {
ZStack {
Button(action: dismissModalSheet.callAsFunction) {
Image(systemName: "xmark")
}
.fontWeight(.semibold)
.foregroundColor(Color(uiColor: .label))
}
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Text("Showing Modal Sheet")
.font(.title3)
Spacer()
}
.padding()
}
}
Create PresentationDetent extension
A straightforward implementation of the .presentationDetents()
method sets the available detents/static properties and methods (snap points) for enclosing sheet presentations in different sizes:
.large
.medium
.custom<D>(D.Type)
.fraction(CGFloat)
.height(CGFloat)
All the above properties and methods return a PresentationDetent
instance. Let's see this in practice in the bellow code by checking the .presentationDetents([.medium, .height(420), .large])
line of code.
Button(action: presentSheet) {
HStack {
Image(systemName: "arrow.up")
Text("Present Sheet")
}
.fontWeight(.semibold)
.foregroundColor(.white)
.padding()
.background(Color.accentColor)
.cornerRadius(8)
}
.sheet(isPresented: $isPresentingSheet) {
ContentModalSheet()
.presentationDetents([.medium, .height(420), .large])
}
By default, sheets present the PresentationDetent/large
detent. If we want to change this behavior we can change this with two lines of code.
- Add a private property named
selectDetent
of typePresentationDetent
and initializes it with a value of.height(64)
.
struct ContentView {
// ...
@State private var selectDetent: PresentationDetent = .height(64)
}
- Add the
selection
parameter to the.presentationDetents
method and assign it with$selectDetent
variable. The method will return the selectedPresentationDetent
assigned to this variable.
// Button... {}
.sheet(isPresented: $isPresentingSheet) {
ContentModalSheet()
.presentationDetents(
[.height(64), .medium, .height(420), .large],
selection: $selectDetent
)
}
To explain a little bit further, a detent is a position at which our modal presentation can snap to. For example, if we have a modal presentation, we can define different detents for displaying the View in different sizes or styles.
Now that we understood how detents work, let's extend our detents to a separate file called PresentationDetent+Extension
, then add the following code to it.
extension PresentationDetent {
static let mediumBottomBar = Self.medium
static let largeBottomBar = Self.large
}
This code defines two static properties in the PresentationDetent
struct and assigns them the original values of the medium
and large
static properties of the struct. This can be good if we want to improve the context and nomenclature of our properties and feature implementations.
In the next section, we will go further and finally implements our custom property.
CustomPresentationDetent implementation
To implement CustomPresentationDetent
for our modal presentation, we need to create a custom struct that conforms with the protocol. The protocol contains a static method that returns an optional CGFloat
.
static func height(in context: Context) -> CGFloat?
So, let's create our custom struct and implements the protocol.
private struct BottomBarDetent: CustomPresentationDetent {
// Calculates and returns a height based on the context.
static func height(in context: Context) -> CGFloat? {
max(64, context.maxDetentValue * 0.1)
}
}
Now on top of our extension file, we can add a new property that uses our custom type.
extension PresentationDetent {
static let bottom = Self.custom(BottomBarDetent.self)
// ...
}
And respectively in our existing .presentationDetents
method, we can update it to use our new custom properties .bottom, .mediumBottomBar
, and .largeBottomBar
.
// Button... {}
.sheet(isPresented: $isPresentingSheet) {
ContentModalSheet()
.presentationDetents(
[.bottom, .mediumBottomBar, .largeBottomBar],
selection: $selectDetent
)
}
Summary
This article provided a detailed explanation of how to use CustomPresentationDetent
. Now if necessary, create more flexible and customizable modal presentations in your future SwiftUI projects.
You can download the final project here.
If you enjoyed this article, please consider supporting me by offering me a coffee:
Thanks for reading, and happy building! :)