CustomPresentationDetent Protocol Implementation in SwiftUI for Modal Presentations

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.

  1. Getting started with the starter project

  2. Create PresentationDetent extension

  3. Implementing 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 the start-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 called ContentModalSheet. The ContentView struct also conforms to the View protocol and has a body property that returns a VStack 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 the Button that dismisses the sheet by using the Environment dismiss 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.

  1. Add a private property named selectDetent of type PresentationDetent and initializes it with a value of .height(64).
struct ContentView {
    // ...

    @State private var selectDetent: PresentationDetent = .height(64)
}
  1. Add the selection parameter to the .presentationDetents method and assign it with $selectDetent variable. The method will return the selected PresentationDetent 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! :)

Did you find this article valuable?

Support Amarildo Lucas by becoming a sponsor. Any amount is appreciated!