Replicating the Safari toolbar collapsing and expanding animation on iOS 15
The iOS 15 update brought us a redesigned Safari app with the address bar being displayed at the bottom of the screen compared to the earlier version where it was on top. The redesign also brought us some delightful animations when creating new tabs or when scrolling web pages where the address bar would expand and collapse. I found the animations quite neat, so I spent some time trying to recreate them.
In this blog post I will explain how I implemented the animation for collapsing and expanding of the toolbar and address bar when the user scrolls a web page. You can take a look at the source code on this GitHub repo. The end result is the following animation:
Toolbar and address bar setup
The Safari toolbar is implemented as a UIToolbar
subclass that contains UIBarButtonItem
s for different actions. When the toolbar is added to the view hierarchy its height is set to 100, so that it can fit the address bar above the UIBarButtonItem
s.
The constraints setup method for the toolbar is written using SnapKit and looks like this:
We are keeping the reference to the bottom constraint, so that we can use it later to animate the disappearance of the toolbar.
The address bar is also a custom class that contains, among other properties, a textfield for entering the website that you want to load and a label for showing the domain name of the website. The address bar is added to the root view and placed on top of the toolbar:
The addressBarExpandingFullyBottomOffset
property is a CGFloat
constant that we will need later on to show the toolbar expanding animation.
Collapsing animation
For implementing the toolbar collapsing animation we can use the UIViewPropertyAnimator
. It is a great way to create dynamic, interactive, interruptable animations and the API is relatively easy to use.
The collapsing animation is composed of two parts. In the first part of the animation the address bar and toolbar are pushed down the screen until they reach a certain threshold. Then the second part of the animation starts which:
- pushes the toolbar below the bottom edge of the screen
- pushes the address bar to the bottom of the screen
- scales down the address bar, so that it gives the effect of it being stretched
- scales down the domain label, so it fits the toolbar in the collapsed state
The animator for the collapsing animation would look like this:
- We need to update the address bar and toolbar constraints, so that we can prepare the first part of the animation where the address bar and toolbar partially collapse.
- We create our animator whose animation block triggers the constraints change animation and address bar alpha animation.
- We add a completion block to the animator which will trigger and complete the second part of the collapsing animation.
- In the second part of the animation we once again update the address bar and toolbar constraints and animate the change using another independent animator. We also update the transform of the address bar to create the stretching animation and update the transform of the domain label to animate the scaling down of the label text.
- We need to call
pauseAnimation()
to move the animator to the active state. We will later manually control the animation by updating thefractionComplete
property.
Expanding animation
The expanding animation is implemented the same way as the collapsing animation - using the UIViewPropertyAnimator
. The expanding animation is also composed of two parts. In the first part of the animation the address bar and the toolbar are pushed up the screen until they reach a certain threshold. Then the second part of the animation starts which:
- sets the toolbar and address bar constraints to the initial value when the toolbar was not collapsed
- reverts the transforms of the address bar and domain label to
identity
. - sets the alpha of the address bar back to 1.
The animator would look like this:
Controlling animations using pan gesture tracking
The collapsing and expanding animations wouldn’t look so good if they weren’t interactive. To achieve that the animation runs dynamically based on the scrolling distance of the web view, we can track the scrolling behaviour of web view’s scroll view using its pan gesture recognizer. The pan gesture handling method would look like this:
When the pan gesture begins we store the initial y offset. Once the gesture changes we need to calculate the y offset difference and use it to activate the right animation and calculate its progress. The webViewDidScroll(yOffsetChange:)
method would look like this:
- We calculate the animation completion fraction using the y offset. We also set the threshold after which the animation should complete automatically.
- If the user is scrolling down we need to trigger the collapsing animation.
- We track the state of the toolbar and address bar using the
isCollapsed
flag. If the toolbar is already in the collapsed state then we skip the animation. - If an animator does not exist (e.g. we just started the animation) we need to create it. Also, if the animation has already completed and the animator moved to the inactive state, but user keeps scrolling without ending the pan gesture then we need to stop and recreate the animator.
- If the
animationFractionComplete
value below thethresholdBeforeAnimationCompletion
then we just update the animation completion fraction. But, if the threshold is reached then we update theisCollapsed
flag and complete the animation by calling thecontinueAnimation(withTimingParameters:, durationFactor:)
method on the animator. - The same logic for collapsing is also used for expanding animation.
Reverting incomplete animations
One neat thing about these animations is that they revert back to the previous state if the animation completion fraction doesn’t reach the threshold before the user stops scrolling.
Initially, I thought that I could implement this behaviour by using the animator’s isReversed
property. Unfortunatelly, this doesn’t work when the animation is implemented using constraints. That’s why I had to use a different approach.
We need to track when the user ends dragging and reverse the animation if it hasn’t completed. As shown in the pan gesture handler above, the webViewDidEndDragging()
method gets called when the gesture ends. Its implementation would look like this:
- If the collapsing animator is active, but the toolbar is not fully collapsed then we need to revert the animation.
- If the expanding animator is active, but the toolbar is not fully expanded then we need to revert the animation.
- Set the animators to
nil
so that they are recreated when the user starts dragging again.
The collapsing animation reversal is triggered by calling the reverseCollapsingToolbarAnimation()
method:
- We need to update the constraints so that their value corresponds to the current layout visible on the screen. We need to use the
fractionComplete
of the animator to determine the completion state of the animation and use it to calculate the value for our constraints. After that we need to trigger the layout of the updated constraints. - We stop and finish the current animator.
- We update the constraints and alpha to their previous value (in this case the expanded state value) and trigger the reverse animation.
The expanding animation reversal method is implemented in the same way:
Conclusion
Working on this project was quite interesting. In this post, I’ve described how the toolbar animations are implemented through a couple code snippets. If you are interested in how the project was organized in more detail you can check out my GitHub repo which contains a lot more content.