You can also read this article directly in my personal blog https://amarildo.codes
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
PresentationDetentextensionImplementing
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
mainbranch contains the final result of the implementation.The
start-projectbranch contains the version that you should use to follow through with this article. It is highly recommended that you use thestart-projectbranch.
The start project contains a ContentView.swift file that:
Defines a SwiftUI view.
Contains a button that, when pressed, toggles the boolean value of
isPresentingSheetand presents a modal sheet using the.sheetmodifier. The content of the modal sheet is defined in a separate struct calledContentModalSheet. TheContentViewstruct also conforms to the View protocol and has a body property that returns aVStackcontaining 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 theButtonthat dismisses the sheet by using the Environmentdismissproperty and calling the.callAsFunctionmethod.
// 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
selectDetentof typePresentationDetentand initializes it with a value of.height(64).
struct ContentView {
// ...
@State private var selectDetent: PresentationDetent = .height(64)
}
- Add the
selectionparameter to the.presentationDetentsmethod and assign it with$selectDetentvariable. The method will return the selectedPresentationDetentassigned 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! :)
Top comments (0)