# How to create Bottom Sheet with UIKit

BottomSheet has been present in iOS applications for a while, with Apple first implementing it in the Maps app in iOS 10. However, it was not available for developers to use as a standard component, so if they wanted to include it in their apps, they had to create their implementation of it.

Apple provided an easy-to-use solution since iOS 15 with [UISheetPresentationController](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller).

```swift
let vc = UIViewController()
// can also use .formSheet and result will be the same on the iPhone
vc.modalPresentationStyle = .pageSheet
if
    #available(iOS 15.0, *),
    let sheet = vc.sheetPresentationController
{
	sheet.detents = [.medium(), .large()]
}
present(vc, animated: true, completion: nil)
```

But on iOS 15 UISheetPresentationController was able to show the bottom sheet only with 2 sizes (detents):

* large (almost full-screen height);
    
* medium (approximately half of the screen height).
    

Check [this post](https://sarunw.com/posts/bottom-sheet-in-ios-15-with-uisheetpresentationcontroller/) for more information.

Only since the iOS 16 it became possible to create [custom detents](https://stuartbreckenridge.net/custom-detent-height-in-ios-16/) with specific preferred heights.

```swift
let smallDetentId = UISheetPresentationController.Detent.Identifier("small")
let smallDetent = UISheetPresentationController.Detent.custom(identifier: smallDetentId) { context in
    return 100
}
sheetPresentationController?.detents = [smallDetent, .medium(), .large()]
```

Using custom detents has given developers more flexibility in implementing bottom sheets in their apps, but it is still not perfect. The **pageSheet** and **formSheet** styles on iPads look different than they do on iPhones (as modals), which can be an issue if you want your component to have the same appearance on all devices. Additionally, the size of the bottom sheet is not automatically determined by its content size, which would be more convenient. Today, let's explore how to implement a desired bottom sheet using UIKit.

(It would be interesting to implement it with SwiftUI as well, but most of the apps still use UIKit, so we will do it with SwiftUI in one of the next articles).

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1674097772571/d9498123-d45b-4813-b827-7a6a516f9888.png align="center")

### **Quick Intro**

If you want to implement Bottom Sheet using custom transition, you need:

1. Set its [modalPresentationStyle](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621355-modalpresentationstyle) property to `custom`;
    
2. Assign an object that conforms to `UIViewControllerTransitioningDelegate` protocol to [transitioningDelegate](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621421-transitioningdelegate) property of UIViewController you want to present.
    

```swift
let vc = ExampleViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = delegate // UIViewControllerTransitioningDelegate
present(vc, animated: true)
```

**UIViewControllerTransitioningDelegate** is a protocol in UIKit that defines methods for supporting custom transitions between view controllers.

**TIP**: The property `transitioningDelegate` of the controller is **weak**, so it is necessary to additionally retain the object with a strong reference.

To implement your transitioning delegate object, you will need to provide to UIKit with different objects related to the animation process, interaction and presentation, such as:

1. **Transition animator object** that conforms to the [UIViewControllerAnimatedTransitioning](https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning) protocol and is responsible for performing the actual animation for the transition.
    
    (**TIP**: You can even provide separate animator objects for presenting and dismissing the view controller).
    
2. **Interactive animator object** that conforms to the [UIViewControllerInteractiveTransitioning](https://developer.apple.com/documentation/uikit/uiviewcontrollerinteractivetransitioning) protocol **optionally** if you want to use touch input or other user interactions to control the timing of the animation.
    
3. The [UIPresentationController](https://developer.apple.com/documentation/uikit/uipresentationcontroller) object is used to manage the presentation and dismissal process, including the position, size, and certain aspects of the presentation, such as displaying a shadow view during a custom transition. The presentation controller acts as a container for the presentation and its lifespan extends beyond the animator objects involved in the presentation, making it very useful in the transition process.
    

### Let’s go to the code

Let’s see how we can implement all objects described above.

```swift
class BottomSheetTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
	
	let transition: BottomSheetTransition    

    func animationController(
        forPresented presented: UIViewController,
        presenting: UIViewController,
        source: UIViewController
    ) -> UIViewControllerAnimatedTransitioning? {
        transition.isPresenting = true
        transition.wantsInteractiveStart = false
        return transition
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        transition.isPresenting = false
        return transition
    }
    
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        transition.isPresenting = false
        return transition
    }
    
    func presentationController(
        forPresented presented: UIViewController,
        presenting: UIViewController?,
        source: UIViewController
    ) -> UIPresentationController? {
        BottomSheetPresentationController(presentedViewController: presented,
                                          presenting: presenting,
                                          configuration: configuration)
    }
}
```

As you can see from the code we provide `BottomSheetTransition` object named **transition** as a:

1. **Transition animator object** for presentation transition (`animationController(forPresented:`) and for dismissal transition (`animationController(forDismissed:`)
    
2. **Interactive animator object** for interactive dismissal (with user’s swipe gesture). We implement only method `interactionControllerForDismissal` for providing interactive animator object for dismiss transition because we don’t need interactive presentation transition.
    

This is how we implement `BottomSheetTransition`:

```swift
class BottomSheetTransition: UIPercentDrivenInteractiveTransition {
    /// `true` if the transition is for the presentation.
    /// `false` if the transition is for the dismissal.
    var isPresenting = true

	/// Set this to `false` in order to start an interruptible transition non
	/// interactively (for example when tap on overlay view)
	var wantsInteractiveStart: Bool
    
    /// The interactive animator used for the dismiss transition.
    private var dismissAnimator: UIViewPropertyAnimator?
    
    /// The animator used for the presentation animation.
    private var presentationAnimator: UIViewPropertyAnimator?
}
```

`UIPercentDrivenInteractiveTransition` is a class from UIKit that conforms to `UIViewControllerInteractiveTransitioning` and provides all the necessary functionality for an interactive transition. It is percent-driven, meaning that the progress of the transition can be accessed and tracked through the `percentComplete` property, which is expressed as a percentage.

```swift
class UIPercentDrivenInteractiveTransition: UIViewControllerInteractiveTransitioning {
     var percentComplete: CGFloat { get }
}
```

On top of that `BottomSheetTransition` also implements `UIViewControllerAnimatedTransitioning` protocol:

```swift
extension BottomSheetTransition: UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
        animationDuration
    }
    
    /// Will get called when the transition is not interactive 
    /// to animate presenting or dismissing of the controller.
    func animateTransition(
        using transitionContext: UIViewControllerContextTransitioning
    ) {
        interruptibleAnimator(using: transitionContext).startAnimation()
    }
    
    /// Will get called when the transition is interactive.
    func interruptibleAnimator(
        using transitionContext: UIViewControllerContextTransitioning
    ) -> UIViewImplicitlyAnimating {
        if isPresenting {
            return presentationAnimator(using: transitionContext)
        } else {
            return dismissAnimator(using: transitionContext)
        }
    }
}
```

As you can see `UIViewControllerAnimatedTransitioning` methods provide `animationDuration` and implement actual transition animations in `animateTransition(using transitionContext:)`and `interruptibleAnimator(using transitionContext:)`.

### UIPresentationController

Another important object we need to provide in `BottomSheetTransitioningDelegate` is `UIPresentationController`. Our presentation controller will handle several tasks:

1. Provide the size of the presented view depending on its content size;
    
2. Decorate the appearance of the presented view controller (by adding the overlay, pull bar, etc.);
    
3. Add a swipe gesture recognizer and use it for interactive dismissal.
    

Next, let’s find out how to implement them.

### Provide the size of the presented view

To provide the size of the bottom sheet, you need to override the `frameOfPresentedViewInContainerView` property.

```swift
class BottomSheetPresentationController: UIPresentationController {
    
    override var frameOfPresentedViewInContainerView: CGRect {
        // calculate presented view size here
    }

	// ...
}
```

The `systemLayoutSizeFitting` method of UIView can be used to determine the optimal size that best fits the constraints and closely aligns with the target size.

```swift
extension UIView {
    /// The size fitting most closely to targetSize in which the receiver's subtree
	/// can be laid out while optimally satisfying the constraints. 
    @available(iOS 8.0, *)
    open func systemLayoutSizeFitting(
				targetSize: CGSize, 
				horizontalFittingPriority: UILayoutPriority, 
				verticalFittingPriority: UILayoutPriority
		) -> CGSize
}
```

This code we will use inside `frameOfPresentedViewInContainerView` property computation:

```swift
let fittingSize = CGSize(width: containerView.bounds.width,
                         height: UIView.layoutFittingCompressedSize.height)
        
let presentedViewHeight = presentedView.systemLayoutSizeFitting(
     fittingSize,
     withHorizontalFittingPriority: .required,
     verticalFittingPriority: .fittingSizeLevel
 ).height
```

We create `fittingSize` with `layoutFittingCompressedSize.height` and set `verticalFittingPriority` parameter to `fittingSizeLevel` to ensure that we will get the smallest possible size and aligns as closely as possible to satisfy the constraints.

### Decorate presented view controller

To decorate our bottom sheet we will override the presentation controller’s methods:

`presentationTransitionWillBegin` - notifies the presentation controller that the presentation animations are about to start;

`dismissalTransitionWillBegin` - notifies the presentation controller that the dismissal animations are about to start;

`containerViewDidLayoutSubviews` - notifies the presentation controller that layout ended on the views of the container view. Here we will layout added subviews.

```swift
    override func presentationTransitionWillBegin() {
        super.presentationTransitionWillBegin()
        
        containerView?.addSubview(overlayView)
        presentedView?.addSubview(pullBarView)
        
        overlayView.alpha = 0

        presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
            guard let self = self else { return }
            self.presentedView?.layer.cornerRadius = self.cornerRadius
            self.overlayView.alpha = 1
        })
    }
    
    override func dismissalTransitionWillBegin() {
        super.dismissalTransitionWillBegin()

        presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] _ in
            guard let self = self else { return }
            self.presentedView?.layer.cornerRadius = .zero
            self.overlayView.alpha = 0
        })
    }
```

As previously mentioned, the UIPresentationController acts as a container for the transition and is the ideal candidate to decorate the presented controller with an overlay and pull bar. The `containerView` and `presentedView` properties of UIPresentationController were used to access the views involved in the transition that needed to be decorated.

### Interactive dismiss with a pan gesture

The final task in the presentation controller is to handle user swipes to enable interactive dismissal. Let's examine the code for this:

```swift
	// The pan gesture used to drag and interactively dismiss the sheet.
    private lazy var panGesture = UIPanGestureRecognizer(target: self,
                                                         action: #selector(pannedPresentedView))
    
    @objc
    private func pannedPresentedView(_ recognizer: UIPanGestureRecognizer) {
        guard let presentedView = presentedView else {
            return
        }
        switch recognizer.state {
        case .began:
		    // Start to dismiss interactively when user begins to swipe
            dismiss(interactively: true)
            
        case .changed:
			// Update the dismiss tansition progress
            let translation = recognizer.translation(in: presentedView)
            updateTransitionProgress(for: translation)
            
        case .ended, .cancelled, .failed:
			// Finish or cancel the dimissal depending on current progress of dismiss transition
            handleEndedInteraction()
            
        case .possible:
            break
            
        @unknown default:
            break
        }
    }

    private func handleEndedInteraction() {
        guard let transitioningDelegate = transitioningDelegate else {
            return
        }
        if transitioningDelegate.transition.dismissFractionComplete > dismissThreshold {
            transitioningDelegate.transition.finish()
        } else {
            transitioningDelegate.transition.cancel()
        }
    }
```

### Conclusion

In conclusion, I would like to highlight that this is not a perfect solution as it does not support scrolling of content if the presented view controller is larger than the screen height. Additionally, navigation controllers inside the bottom sheet are not supported. However, it is still a simple and clean option if you need to add a bottom sheet to your project.

You can find all code [here](https://github.com/vitalybatrakov/BottomSheet).
