How to Create an Interactive Bottom Sheet in Swift 5
By: Zafar Ivaev
Tags: Mobile, Xcode, Swift, iOS, Programming

Let’s Start

First, we need to create a generic BottomSheetContainerViewController that will contain a content view controller and a bottom sheet view controller as children:

As we can see, both Content and BottomSheet types have to be UIViewController s, which means we will be able to specify any custom UIViewController either as Content or BottomSheet . You will see this later when we start using the BottomSheetContainerViewController in an example.

Now that we have Content and BottomSheet view controllers as children, let’s add some required properties. In this step, we will create two:

A BottomSheetConfiguration struct that will signify the total height and the initial offset of the bottom sheet

struct that will signify the total height and the initial offset of the bottom sheet A BottomSheetState enum, which manages the state of the bottom sheet. It has the .initial and .full cases:

Note that we updated the initializer to include the BottomSheetConfiguration as a parameter.

Now, let’s move to properties that handle interaction and animation. Add the panGesture and topConstraint properties as follows:

The bottom sheet view controller will move around the screen, so we need to get a hold of the top constraint of its view. For this reason, we have the topConstraint property, which we will repeatedly change and animate accordingly.

Note that we conformed the BottomSheetContainerViewController to UIGestureRecognizerDelegate . This allows us to add the method gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) , in which we return true .

This is useful in case you have a UITableView or any other scroll view placed inside the bottom sheet. If we hadn’t added this method, our UIPanGestureRecognizer would stop working when performing a gesture on a UIScrollView subclass.

Now let’s actually add the children view controllers to the container:

Here is a breakdown of all steps:

Add both the contentViewController and bottomSheetViewController to the container using the addChild() method. Add the root views of contentViewController and bottomSheetViewController to the root view of the container. Add the panGesture to the root view of the bottomSheetViewController . Apply translatesAutoresizingMaskIntoConstraint = false . This is required because we are creating our UI programmatically using constraints. Set constraints for the contentViewController ’s view. Call the didMove(to:) method to inform the contentViewController that it was added to the parent. The parent is the BottomSheetContainerViewController . Set the top constraint of the bottom sheet to be aligned to the bottomAnchor of the container’s view. We also add an offset of the BottomSheetConfiguration to make the bottom sheet a little bit visible in the bottom of the screen. Set all the bottom sheet’s constraints and activate them. Call the didMove(to:) method on the bottomSheetViewController to inform it that it was added to the BottomSheetContainerViewController .

Finally we are done with setting up the container and its children. Now let’s handle interactivity using the panGesture .

Let’s connect the panGesture with its action:

Now, we are going to add two methods:

showBottomSheet(animated:) — moves the bottom sheet to its full height and sets the BottomSheetState to .full . If animated is set to true , it performs the movement with animation.

— moves the bottom sheet to its full height and sets the to . If is set to , it performs the movement with animation. hideBottomSheet(animated:) — similarly, this method moves the bottom sheet to its initial point and sets the BottomSheetState to .initial . If animated, it performs a nice spring animation.

The above code shows how you can animate constraints. These are the steps:

Change the constant of the constraint.

of the constraint. Run self.view.layoutIfNeeded() inside an animation block. Note that you must call the layoutIfNeeded() on the superview of the view you want to animate. In our case, this superview is the BottomSheetContainerViewController ’s root view.

Great! Now we can programmatically move and hide the bottom sheet. It’s time to work directly with the UIPanGestureRecognizer ’s action.

Here is the behavior we want to achieve:

Move the bottom sheet with our finger up and down.

When we stop moving it, take into account the current BottomSheetState , translation , and velocity in the y direction. If the state is .full , check if we have the bottom sheet translation of at least half of its height. If this is true , we run the hideBottomSheet(animated:) method. Otherwise, we return it to the full height. Also, we need to check if the magnitude of the velocity is greater than 1,000. If this gives us true , hide the bottom sheet. In other cases, revert it to the full height. Similarly, if the state is .initial , check the translation and velocity magnitudes and react accordingly.

Everything is going to make more sense soon, I promise. Let’s start with obtaining translation magnitude and velocity in the y direction:

Now let’s create a switch for the current state of the gesture:

As we can see, we handle the .began , .changed , .ended , and .failed states of the UIGestureRecognizer . Inside each of them (we group the .began and .changed ones) we provide an if else statement based on the current BottomSheetState .

Let’s handle each case now. First we start with the grouped .began and .failed case:

Here is a breakdown of the above steps. If the BottomSheetState is .full :

Assert that the user scrolls downward. The bottom sheet is already at its full height; no upward scrolling is allowed. Change the topConstraint ’s constant to match the current position of the user’s finger. For example, if the total height of the bottom sheet is 500 points and the user has scrolled 100 points, we subtract 100 from 500 and obtain 400 points. So we set the constant to -400, because this is an offset from the bottom of the container’s view. Update the root view to show the constraint’s change.

In case the BottomSheetState is .initial :

4. Calculate the new constant by using the BottomSheetConfiguration ’s initialOffset and the translation magnitude. For example, if the initial offset was 80 points and the user has scrolled 200 points, we obtain 280 points. This means we would need to place the bottom sheet 280 points from the bottom of the container’s view.

5. Assert that the user scrolls upward. Because the bottom sheet is at its initial point, no downward scrolling is allowed.

6. Assert that the magnitude of the newConstant is less than the full height of the bottom sheet. This is to prevent the bottom sheet from moving further than its maximum height point.

7. Set the resulting constant.

8. Update the root view to show the constraint’s change.

With the .began and .changed states done, now we need to handle the .ended case:

If the BottomSheetState was .full when a user stopped moving the bottom sheet:

Check if the user has moved the bottom sheet at least half of its maximum height, or the y velocity is higher than 1,000. If this is true, hide the bottom sheet. Otherwise, return it to its max height point.

On the other hand, if the BottomSheetState was .initial :

4. Check if the user has moved the bottom sheet at least half of its maximum height, or the y velocity is less than -1,000.

5. If this is true, show the bottom sheet.

6. Otherwise, hide the bottom sheet.

We need to handle the last case, .failed , if the UIPanGestureRecognizer fails during the process:

This time the logic is very simple. If the latest BottomSheetState is .full , return the sheet to its maximum height point. Otherwise, hide the bottom sheet.

Great! We have finally implemented a reusable class. We are now able to use it effectively whenever we want. Let’s quickly use it on a simple example.