Making the App Store iOS 11 Custom Transitions — Part 1 (Presentation)

aunnnn
4 min readApr 22, 2018

--

Just give me the code!

An attempt to replicate the custom transition

There are two parts involved: presenting and dismissing. We will do it both with pure UIKit APIs: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning, and UIPercentDrivenInteractiveTransition.

In part 1 (this article), I will show you how to do presentation animation. The upcoming article will be about interactive dismissal transition.

Quick UIKit’s transitioning delegate recap:

  • UIViewControllerAnimatedTransitioning is the delegate that performs custom animation. You have access to toView, fromView, containerView that you can apply some fancy animations inside func animateTransition(context:).
  • UIPercentDrivenInteractiveTransition is the object that can control progress of the animation. You connect it with some gesture recognizers, and control the progress with update(percentComplete: CGFloat), cancel(), or finish().
  • UIViewControllerTransitioningDelegate binds above two delegates together by asking for the transitioning objects for presentation and dismissal, which most of the time is the same object.

First thing first

To prevent confusions, let’s get familiar with these words:

  • Home page is the first page you see in Today’s tab, containing the list of card cells.
  • Card view is the card-like view. It’s the card you see in the home page. It’s also the view at the top of the Detail page. It’s also a card view used during animation.
  • Card cell refers to each collection view cell in the home page. It wraps a card view inside.
  • Detail page is the detail view controller that shows up after you tap a Card cell.
  • Detail content view is the content below the Card view (e.g., UITextView)

Presenting

For presentation, we can focus only on custom animation because there’s no interactivity involved.

The card’s title and detail must stay at the corners while the card expanding animation is going on. If AutoLayout is setup correctly, we don’t have to do anything.

To make the card’s background image stays same size throughout the animation, use the center for image view's contentMode.

Create a new card view instance specifically for doing custom animation. Let’s call this animatingCardView. Use the same view model as the card cell in the home page. We can’t use snapshot because we want to animate the AutoLayout constraints. Hide the card cell and the detail page (destination) throughout the animation.

Set animatingCardView start position to the tapped cell’s position. Do this by saving the cell’s frame relative to the UIWindow:

let cellOriginFrame = cell.superview!.convert(cellFrame, to: nil)

Next, get the destination frame of the card by:

let destinationWidth = detailVc.cardContentView.bounds.width
let destinationHeight = detailVc.cardContentView.bounds.height
let destinationCenter = detailVc.cardContentView.center

Note: detailVc is the destination view controller you get from transitioning delegate context, and detailVc.cardContentView is the card view on the detail page (same class as any card views).

Bouncing upward animation

Notice a little bounce upward as the card expands

You will notice that there’s a little bounce upward in the card expanding animation.

At first, I thought simply animating the edges constraints with native spring animation will work. However, it’s not that simple.

If you watch closely, you will notice that the expanding part and the bouncing upward part are performed independently.

From my experience (and many experiments), the way to go is to have a spring animation for centerYAnchor, and a linear animation for the cell expanding (widthAnchor/heightAnchor) animation.

However, there is no known way (yet) to “control” different AutoLayout animation on the same view. You call view.layoutIfNeeded() under the animation block and it animates to the latest constraints you set and that’s it.

Updated: Just found a way to animate different constraints separately, check out this article!

Workaround

To solve this, we can create a special container view that contains animatingCardView. Let’s call that animatingContainerView. We will do card’s expanding animation inside the container, and then animating that container’s topAnchor to reach the destination.

Spring animation for animatingContainerView (brown) centerYAnchor. Linear animation for the card (green) widthAnchor and heightAnchor.

Note that the card (green) simply sticks at the top and center horizontally inside animatingContainerView.

Then we animate them separately by calling layoutIfNeeded() on the right container views:

A Better Way

Found later, that there’s a cleaner approach to achieve this without a container view, which is to animate constraints separately:

Conclusion

There are too many details to write all in words. For example, how the highlighted state of the card cell when we press down is so responsive to touch (Hint: Must set delaysTouchInView = false). How font size of multiple card views must match from the beginning to the end of the animation (must set the animating card view’s font size to match the font size is highlighted state), and many more.

If you are curious, check out the code:

Stay tuned for part 2 — interactive dismissing!

👏 If you find this helpful. Tell me what you think!

--

--