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
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
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
Now let’s get back to the
UIScrollView method of
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
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
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.
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:
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
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
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
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: