Creating Pull to Dismiss / Pull to Reach animation using Swift
One of my most used app on my iPhone is Bear app. It’s a beautiful and simple notes-taking app with tons of neat UI / UX features. One of which is being able to pull down on a scroll view to dismiss a view or select a button. As phones are getting taller, buttons on the top navigation bar are getting difficult to access using just one hand. Such gestures help users access these buttons just by swiping down or scrolling down on a scroll view which is embedded in collection view or tableviews by default.
Not only accessing navigation items, you can dismiss views containing scroll views by similar gestures. I decided to create such an animation and in this post I will take you through the entire process of analyzing the animation and then building it from scratch.
Complete project is available on GitHub. I have also incorporated protocol / delegate so that I can communicate between the pop up view and the controller. More on this later.
The project also contains some code through which I am setting up the view controller, applying dark view on the background and animating the pop up view as the user taps on the “Action” button in the top navigation bar. We are going to dismiss this view by using scroll view which is embedded in that view.
I am creating the pop up view in a separate file that goes by the name of ActionMenu
. I have wrapped it in a NSObject subclass. The entire animation of the pop up view appearing from the bottom of the screen, creating the UI components and the entire code for this particular animation is written in this NSObject
class file. Through the use of encapsulation, we are creating an instance of this class in the view controller and calling the configureActionMenu()
and showActionMenu()
functions when the “Action” button is tapped.
The code in the UIViewController
file to call the ActionMenu
when the action button is tapped.
and then in the selector function of handleActionButton()
:
Now lets get into the real animation code. I have embedded a scroll view in the action view. I am also conforming the ActionMenu
class to the UIScrollViewDelegate
so that I can use some Scroll View delegate methods.
The first method we will use is this:
So when ever you scroll the scrollView this method will be called. Now before we go any further let’s dissect this animation. The highlight animation works with having two buttons stacked on top of each other. The button on the foreground has a mask layer on it. Remember: When you have a masklayer assigned to a UIView layer then that view alpha becomes 0. So essentially when this mask layer is on the foreground button we can only see the button behind it. Next we will animate this mask so that it goes away as the user scrolls so that we can see the foreground button.
So first set up the two buttons and apply the constraints:
Constraints for the views:
Note how I have created a maskLayer of class CAShapeLayer
. I will assign this layer to the cancelButtonForeground
.
Now let’s get back to the UIScrollView
method of scrollViewDidScroll()
:
Now lets see what happening in this method:
First we are setting a goal
. How many pixels should the user scroll down for the animation to operate. I have set it to 80. Then we will use scrollView.contentOffset.y
to get the value as the person scrolls down. This value is negative, so we will convert it to positive by multiplying it with -1. ContentOffset
, put simply, tells us how much content has the user scrolled. Pulling it downwards provides us with values in negative.
Next we are measuring the progress
. To do this we are dividing the contentOffset
by the goal
. This should give us value in decimals. Starting from 0 to more than 1. We want to make sure that the progress is between 0 and 1. To do this we will use max
and min
methods in swift to restrict the value. If the progress value is more than 1, then return value of 1. If the progress is between 0 and 1 then return the progress
value.
min(1, progress)
return the progress value which is minimum. If progress is more than 1 return 1.
max(0, min(1, progress))
what ever the value we get from the min function, return the max value between it and 0.
Next we have created a custom updateActionMenu
function that takes in the finalprogress
as the parameter. This finalprogress
will be a CGFloat
item between 0 and 1. Now we can use these values to animate the mask layer.
This updateActionMenu
function gets the progress value. We use that progress value to create a path for the mask. Currently our mask has no path so therefore it is hiding the foreground button fully. We will give it a path by constructing it using UIBezierPath
. We will create it using CGRect, and change the height of the rect using the progress value.
Use this height when create the buttonRect.
Create the mask from the UIBezierPath
and add that path as .cgPath
to the maskLayer.path
. Essentially as the height of the mask increase, it will show the UIView
layer behind it. When the progress becomes 1, the entire button will be visible.
So with this amount of code, we get this:
Hoorayy! 🎉
However, we are still not done. We also need to increase the scale the button as we are scrolling. This means that we have three animation states.
notStarted
- progress value should be 0 at this point.
highlightFull
- progress value can be anything at this point - I went for 0.75. By this progress value, the foreground button should be visible.
finalFull
- progress value should be 1 here. Between highlightFull
and finalFull
- the scale value of the button should change
Let’s implement these changes, starting with progress. So now we want to completely highlight the button when the progress is 0.75.
Going for a pretty similar logic as before. I am dividing the progress
by newgoal
of 0.75. The value of newProgress
is between 0 and 1. Again I am using the max and min methods to make sure that the max value is 1 and the min is 0.
Then I am using the finalProgress
variable to change the height of the mask path. So now by the time 75% of the 80 pixels have been scrolled the highlight animation is fully completed. Now when the progress value is more than 0.75 we want to scale up the button.
To do this we will use switch statement. In the first case we are checking if the progress is greater or equal to 0.75 and less than 1. case _ where progress >= 0.75 && progress < 1:
The code that follows is where we are changing the scale.
The goal for the scale is 1.3. We want to increase the size of the button 1.3 times on both X and Y axis. Now we want to calculate the scale value as the person scrolls. So first get the progress value which will again be between 0 and 1. This time we are sure it will not cross 1 so there is no point of using min and max methods here.
The scaling is done by subtracting the goalScale
(1.3) from 1 and multiplying it with newProgress
which will be a value between 0 and 1. Then add 1 to it since we want to enlarge it. Use the transform property and assign it CGAffineTransform
.
In the case ...0.75
block we will make sure that the transform value is .identity
or 1. The three … dots that precedes the case
means that the progress value should be less than 0.75.
In the last case 1
block, we will call handleDismissGesture()
that dismisses the pop up view.
and we are now done! 🎉🎉
This is what we get now: