Bottom sheet on iOS15 using UISheetPresentationController and Xcode 13
Tags: ios, swift
Published: 2021-06-08 09:02:42

At least a couple of times I have faced the requirement of implementing a component that became incredibly popular across iOS apps design starting with the Maps app. With no out of the box iOS component it was always tricky to get this thing working in the way that covered all possible use cases in clear way. Thankfully, we just get the `UIPresentationController` subclass that gets these things done for us.

Here is how Apple docs introduces that new class:

> *UISheetPresentationController* lets you present your view controller as a sheet. Before you present your view controller, configure its sheet presentation controller with the behavior and appearance you want for your sheet.

> Note: You will need Xcode 13 (currently beta) and iOS15 device or simulator to run the code from this article.

Let's create our initial controller like that. It will have a text label and a bar button item triggering some simple controller presentation that we will implement a little bit later.

```swift
class ViewController: UIViewController {

    private var label: UILabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        navigationItem.title = "Sheet Demo"
        navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "pencil.circle.fill"),
                                                            style: .plain,
                                                            target: self,
                                                            action: #selector(showSheet))
        setupLabel()
    }

    @objc
    private func showSheet() {
        // TBD
    }

    private func setupLabel() {
        label.text = "Some Awesome Text"
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}
```

Let's also define some other controller that we will use to edit the value. 

```swift
class FormController: UIViewController {

}
```

We are going to connect them using the simple delegation defined by the `FormControllerDelegate` protocol.

```swift
protocol FormControllerDelegate: AnyObject {
    func formControllerDidFinish(_ controller: FormController)
}
```

Apart from the delegate property, we will expose the `text` property allowing us to get and set editable text as well as the button allowing us to save content while we are done.

```swift
class FormController: UIViewController {

    var text: String?

    weak var delegate: FormControllerDelegate?

    private var textField = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        navigationItem.title = "Some Modal Form"
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save,
                                                            target: self,
                                                            action: #selector(save))
    }

    @objc private func save() {
        // TBD
    }
}
```

The `FormController` is going to have the `UITextField` allowing as to change the provided `text`. The complete class looks as below

```swift

class FormController: UIViewController {

    var text: String? {
        didSet {
            textField.text = text
        }
    }

    weak var delegate: FormControllerDelegate?

    private var textField = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        navigationItem.title = "Some Modal Form"
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save,
                                                            target: self,
                                                            action: #selector(save))
        setupTextField()
    }

    private func setupTextField() {
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.addTarget(self, action: #selector(handleNewText), for: .editingChanged)
        view.addSubview(textField)
        NSLayoutConstraint.activate([
            textField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            textField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8),
            textField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8)
        ])
    }

    @objc func handleNewText() {
        text = textField.text
    }

    @objc private func save() {
        delegate?.formControllerDidFinish(self)
    }
}

```

Now, it's time to implement the previously not defined `showSheet()` method. First we are going to instantiate all the controllers we are about to present

```swift
let formController = FormController()
formController.delegate = self
formController.text = label.text

let formNC = UINavigationController(rootViewController: formController)
```

Now we need to assign the proper `modalPresentationStyle` on `UINavigationController`.

```swift
formNC.modalPresentationStyle = UIModalPresentationStyle.pageSheet
```

That way we will get the proper presentation controller set, so we can get it

```swift 
if let sheetPresentationController = formNC.presentationController as? UISheetPresentationController {
...
}
```

We are now able to customize the way how it looks and behaves. For example, we may set the visibility of the grabber and allow our sheet to be sticky on particular heights.

```swift
// Let's have the grabber always visible
sheetPresentationController.prefersGrabberVisible = true
// Define which heights are allowed for our sheet
sheetPresentationController.detents = [
    UISheetPresentationController.Detent.medium(),
    UISheetPresentationController.Detent.large()
]
```

The complete method looks like that. Now the code is complete and we can try running it. 

```swift
private func showSheet() {
    let formController = FormController()
    formController.delegate = self
    formController.text = label.text

    let formNC = UINavigationController(rootViewController: formController)
    formNC.modalPresentationStyle = UIModalPresentationStyle.pageSheet

    if let sheetPresentationController = formNC.presentationController as? UISheetPresentationController {
        // Let's have the grabber always visible
        sheetPresentationController.prefersGrabberVisible = true
        // Define which heights are allowed for our sheet
        sheetPresentationController.detents = [
            UISheetPresentationController.Detent.medium(),
            UISheetPresentationController.Detent.large()
        ]
    }
    present(formNC, animated: true)
}
```

The effect we achieved with just a couple lines of Swift is really nice:

![UISheetPresentationController example](https://matrejek.eu/images/sheet-demo.gif)

This seem to be a really great and long awaited addition to the UIKit. Be sure to dive deeper in it's API [here](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) as it offers some additional behaviour customizations that are surely worth checking!