CustomPresentationDetent Protocol Implementation in SwiftUI for Modal Presentations
Tags: swift, swiftui, ios, xcode
Published: 2023-03-08 21:47:29

> 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](https://github.com/amarildolucas/CustomPresentationDetent/tree/start-project) that explains how to create an extension of `PresentationDetent` *struct* and implements `CustomPresentationDetent` *protocol*.

1. Getting started with the [starter project](https://github.com/amarildolucas/CustomPresentationDetent/tree/start-project)
    
2. Create `PresentationDetent` extension
    
3. Implementing `CustomPresentationDetent`
    

## Getting started with the starter project

Before we start, *clone/fork* or [download](https://github.com/amarildolucas/CustomPresentationDetent/tree/start-project) the project from the `main branch` and run the following `git` commands:

```bash
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.
    

```swift
// 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.
    

```swift
// 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.

```swift
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)`.
    

```swift
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.
    

```swift

// 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.

```swift
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`.

```swift
static func height(in context: Context) -> CGFloat?
```

So, let's create our custom *struct* and implements the *protocol*.

```swift
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.

```swift
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`.

```swift
// 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.](https://github.com/amarildolucas/CustomPresentationDetent/tree/main)

If you enjoyed this article, please consider [supporting me by offering me a coffee](https://www.buymeacoffee.com/amarildocodes):

* [https://www.buymeacoffee.com/amarildocodes](https://www.buymeacoffee.com/amarildocodes)
    

Thanks for reading, and happy building! :)