Custom Navigation Transitions, Part III: A Complex Push/Pop Animation
May 22, 2019
– iOS, animation, code
This is the third in a series on building navigation transitions for iOS apps.
In the first post, we
simplified the API and learned about its key protocols. In the second
post, we built a simple
modal animation, and learned about UIViewPropertyAnimator.
Today, we’ll be building a complex push/pop navigation transition from my app,
Locket Photos. This animation mimics the transition in
the native Photos app: you select an image from a grid, and it grows to
fill the screen, coming to rest in the photo-detail screen. (We’ll get to the swipe-to-dismiss interactive-pop animation in the next post!) We’re going to do a detailed walkthrough of building the push transition and then (spoiler!) we’ll find that building the pop transition is extremely similar.
Here’s the push/pop transition animation we’ll be building today:
Note: This is a long post. I’m trying to walk through a non-trivial
implementation of a navigation-transition animation, and that means addressing
a dozen-or-so details along the way. Bryan Hansen
once did this in an amazing UICollectionView
tutorial,
and I found it immensely helpful — so I’m trying to do something similar here:
demonstrate a production-quality, non-trivial implementation.
Planning the push animation
The overall idea of this animation is that a little photo-thumbnail should grow
up to fill the screen when you tap it. Simple, right? Not so fast: there are a
lot of details we’ll need to handle to avoid a glitchy animation — and it’ll
often take several implementation iterations until we can get things running
smoothly.
Here’s what we need in our push animation:
the selected image scales up and moves to its position on the
photo-detail screen,
the tab bar slides away.
Great! But wait! Not so fast! We don’t want to actually move the
UIImageView from the grid to the photo detail screen — that would mean lots of
code to pull out that image view, and insert it deep into the photo-detail
screen’s view hierarchy. Instead, we’ll create a temporary third UIImageView,
and hide the grid and photo-detail images during the animation.
Excellent! Oh wait, ugh, one more thing… If we just use the little image from
our photo-grid and scale it up, then we’ll have this weird, scaled-up pixelated
thing that flickers into the final spot. Instead, we’ll want to try and fetch
the image at a larger size, if we have that cached via the Photos API.
Magnificent! Ahh wait, one more snag. The animation we want (where the tab
bar slides down offscreen) isn’t something available via UIKit — so we’re gonna
have to figure out something else there, that doesn’t mess up our
safeAreaInsets on the collection grid or the photo-detail screen.
😅 See how these details add up? This was my experience in building this
animation transition — and each one of those paragraphs involved building the
animation, realizing it was glitchy, updating my design plans for the
animation, exploring options, and rebuilding the animation. So: if you’re
doing this in your app, do not be hard on yourself if this pattern happens to
you — this custom-animation dance is one of the trickier parts of UIKit, but
you can do it - be patient!
Here’s our final list of what we’ll implement in the animation:
we create a temporary UIImageView, and set its image to be the larger-size
one we’ll want in the photo-detail screen,
we’ll place this temporary image in the spot of the cell that the user has
just tapped,
we’ll hide the other images (the thumbnail that the user
tapped, and the one in the photo-detail screen) so there’s only one image
during the transition,
the transition image scales up and moves to its position on the
photo-detail screen,
the tab bar slides away,
then once the animation completes, we un-hide the photo-detail image, and
remove the transition image,
and un-hide the collection cell back on the grid screen.
Alright, that’s a healthy list! Let’s build it!
Composing the push animation
In the first post in this series, I drew some diagrams that showed different
ways of composing these animations. Here are the parts of that diagram that we’ll build in this post:
We’ll have a UINavigationController that conforms to
UIViewControllerTransitioningDelegate — this is the “vending machine” that
creates the PhotoDetailPushTransition.
We’re going to create a separate class, PhotoDetailPushTransition, that will
implement the UIViewControllerAnimatedTransitioning protocol.
We’ll also build a protocol, PhotoDetailTransitionAnimatorDelegate, (not
pictured) that will allow our PhotoDetailPushTransition to show/hide the image
views on the photo-grid and photo-detail screens.
Outlining the push animation in pseudocode
Let’s rough out the implementation, and we’ll fill in the details as we go.
First, we need to implement UIViewControllerTransitioningDelegate, where
we’ll vend an animation every time a photo-detail screen is shown. For Locket, I
chose to create LocketNavigationController, a subclass of
UINavigationController that just adds the ability to create transition animations.
Second, we need to build out the animation itself - let’s call it
PhotoDetailPushTransition:
Nice! We’ll have to build out the animation, but let’s keep sketching out our
pseudocode, and we’ll come back to fill in the details.
Third, we need to coordinate the animation’s effects — we need to create
that “transition image” that appears to move between screens, manipulate the tab
bar, and do it in a way that doesn’t tightly-couple our screens’ code.
To do this, I chose to create another delegate protocol:
PhotoDetailTransitionAnimatorDelegate, which we’ll implement in our
photo-detail and photo-grid screens. It gathers snapshots of image views (and
their location onscreen), and notifies the view controllers when the transition
will start, and when it’s finished.
Fourth, we’re going to need a way to animate the tab bar up and down.
There’s not (at least, as far as I’m aware) a way to get the system to perform
this animation for free, so I’ve got LocketTabBarController, a subclass of
UITabBarController, that performs this animation.
Note: remember last time, how I said we’ll be using UIViewPropertyAnimator,
because it makes it easy to coordinate animations between view controllers?
This is a perfect example: later in this post, we’ll be coordinating the
tab-bar’s show/hide animation with the rest of the animations, by passing in a
property animator and attaching more animations to it!
So that’s our high-level pseudocode: from LocketNavigationController, we’ll
vend a PhotoDetailPushTransition animation to UIKit. We’ll make our photo-grid
and photo-detail screens conform to PhotoDetailTransitionAnimatorDelegate. We
also have LocketTabBarController to show and hide the tab bar appropriately.
Filling in the push animation’s details
Now that we’ve got the broad outline, let’s go back in and finish up those
TODO bits.
First, we’re going to fill in a little detail in our
LocketNavigationController. We need to provide the photo-grid and photo-detail
screen to PhotoDetailPushTransition, so we can create and animate the
transition-image properly.
Second, we’re going to fill in our PhotoDetailPushTransition. It’s now
going to keep a reference to the photo-grid and photo-detail screens, and we’ll
be able to implement the animateTransition!
Third, we’re going to need to implement our
PhotoDetailTransitionAnimatorDelegate protocol in the photo-grid and
photo-detail screen.
Fourth, we’ve got to build out the LocketTabBarController, which animates
the tab bar up-and-down in the animation.
They’re so similar, in fact, that you might even consider refactoring PhotoDetailPushTransition and PhotoDetailPopTransition to be the same class — but I’ll leave that as an exercise to you! ↩