Adding a Customizable Swipe Down to Dismiss Functionality to any ViewController
Utilizing PanGestureRecognizer to create a reuable pull to dismiss functionlity.
As many of you probably know, the swipe down to dismiss kinda already exists and is provided by UIKit. However, there’s a catch to it — the view controller in question has to have its modalPresentationStyle be one of the following; automatic
, formSheet
, pageSheet
, or popover
. For any other UIModalPresentationStyle
, we are forced to write some fun custom code. In the upcoming part, I’ll go over how we can create a customizable swipe down to dismiss functionality using a simple UIPanGestureRecognizer
. Additionally, we’ll add this functionality as an extension to UIViewController
, allowing us to add it with just about two lines of code.
The Flexibility
I dropped the term customizable in the title, so let’s try to make it that. We’ll create a struct called SwipeDownConfiguration
, which we’ll use to keep a reference to all the customization that will be added. We’ll also create various conditions that need to be met for the view to be dismissed, otherwise enabling it to bounce back into place if those conditions haven’t been met. Here are the list of things we will add:
- An option to add resistance to the pan gesture, so that the view would drag half the amount is should. We’ll call this variable
isSticky: Bool
- An option for the view to fade out relative to its y-coordinate offset. We’ll name it
shouldViewFade: Bool
- An option to specify how fast (in seconds) the view would dismiss or retract back if the condition for dismiss is met/not met. We’ll call this
animationDuration: CGFloat
- One of the conditions we’ll use to determine if to dismiss the view is the velocity of the pan gesture. We’ll call it
minimumVelocityToDismiss: CGFloat
- Another condition we’ll use is the minimum vertical offset from the top of the screen. We will use percentage values from 0.0–0.9 (decimals to keep it simple). An example can be seen below. We’ll call this
minimumScreenPercentageOffsetToDismiss: CGFloat
- The last thing we will add is a closure so we can notified when status of the pan gesture is changed. We will create an enum for all the possible statues, called
SwipeDownStatus
. And we will call this variablestatusChange: (SwipeDownStatus) -> Void
.
Here are all the possible values for SwipeDownStatus
:
initiated
: pan gesture ended, conditions have been met, the view will begin to dismisscompleted
: view is dismissed and completely out of viewcancelled
: pan gesture ended but conditions were not met, the view will revert back to original position
The Pan Gesture
Next comes implementing the function that will be used by our UIPanGestureRecognizer
, which we will create later. Basically, we have to move the view along with the pan gesture, alter the view based on any customization from theSwipeDownConfiguration
, and detect if the conditions to dismiss have been met. For the code below, imagine we created a getter for SwipeDownConfiguration
, but that part will be discussed later. Below is the onPan()
function, any comment that includes a number will be discussed in more detail.
- If we can’t extract a configuration from the imagine getter, something is wrong, and we abort.
- We create a closure that will be responsible for transforming the view vertically, as well as adding any fading effect, if applicable. The parameter to for the closure will be the vertical offset from
UIPanGestureRecognizer
. We will use a simpleCGAffineTransform
to move the view. - Another closure, this one is dedicated to notify
statusChange()
with.cancelled
status, and it animate back the view to its original position. - We start examining the
panGesture.state
. For the.began
and.changed
state, we only care about transforming the view vertically, so we use themoveViewVerticallyTo
closure from before. Note, we use theadjustedTranslationY
in caseconfiguration.isSticky
is true. - If the gesture is in the
.ended
state, we determine if the conditions for dismiss have been met. We calculate the velocity and reference the configuration variables to determine is the conditions have been met. - If the conditions have been met, we notify
statusChange()
with.initiated
status, and trigger an animation for the view to go completely out of view. When that animation is complete, we notifystatusChange()
with.completed
status. - If the conditions have not been met, we reset the view using
cancelAndReset()
. - In the
.undetermined
state of the gesture, we just revert back to original position, usingcancelAndReset()
.
The Key For Recyclability
It would be great to make this pan gesture code reusable without having to copy pasta it to every file we need it in. One approach we can take is to create a protocol that requires variables for a pan gesture and the configuration. Then have default methods that connect everything together. But we can go a step further to keep things simple, without using a protocol. We will create an extension on UIViewController
and use the magical associated objects. If you want to read more on them, here is a great explanation by Mattt. Basically we can create associated objects to store the pan gesture and the configuration inside an extension. We will create computed properties for both, with custom getter and setter to set up the respective associated object methods. This is the imaginary getter we used in the onPan()
to refrence SwipeDownConfiguration
.
The Extension
Now to combine everything, we can go ahead and create our extension. We need two methods, one to add the swipe down to dismiss with configuration struct as a parameter, and another one to remove any existing swipe down to dismiss functionality. The latter will come in handy in the former, to make sure anything lingering around is removed properly.
- For the add method, we need to create a
UIPanGestureRecognizer
, add it to theview
, and assign it along with the configuration using the associated objects created above. - For the remove method, we need to make sure we remove the pan gesture from the
view
and clear both the associated objects.
The Concoction
With everything created, all we have to do is create a file and combine all the code above. We would need to do a little upkeep and make all the associated objects and onPan()
code private, and just leave the add and remove methods public for the extension. Here is a gist of the complete code. Now we are able to add this functionality to almost any UIViewController
with only two lines of code (well more if you want to make it look pretty):
The Uncertainty
There are certain scenarios where this approach will not work nor is best suited for. Here are a couple of those scenarios:
- Calling
addSwipeDownToDismiss(...)
on a subclass ofUIViewController
that has itsview
be anything besides a plainUIView
. For example, aUITableViewController
with itsview
being aUITableView
. - If you don’t want to add swipe to directly to the
view
of theUIViewController
. Though the code above can be tweaked make it more customizable. - If the
UIViewController.view
contains any scrollable subview. Also if theview
has any other pan gesture recognizers that might interfere with the our pan gesture. In the below example, you can see aUIScrollView
nested insideUIViewController.view
, which is nested in aUINavigationController
. With theaddSwipeDownToDismiss(...)
added to theUINavigationController
, you can see only only the navigation bar reacting to the pan gesture.
The End
I think that covers it all. The gifs used throughout this article are recorded from a demo app, which displays a simple UI to customize values for SwipeDownConfiguration
. Allowing one to test the swipe down to dismiss with all various different configurations. The demo app, along with all the code can be found in this repository. Danke for reading!