Animations: Bringing the Host Passport to Life on iOS

How Airbnb enabled hosts and guests to connect and introduce themselves through the Host Passport.

By: Anne Lu

Introduction

In May 2023 we introduced the Host Passport as part of our Summer Release. We wanted to give Hosts a way to introduce themselves, and start building a more personal connection with their guests. To that end, we created the Host Passport, which appears in the bottom corner of each Private Room listing result with a photo of the Host on the cover. Guests can tap it to fully open the Host Passport and learn more about the Host and get a sense for the real live person they would be staying with.

The Passport animation

The Host Passport offers Hosts a way to introduce themselves and set guest expectations, and allows guests to quickly start discovering who they could be sharing a space with.

Delivering this animation with high pixel accuracy, fluidity, high performance, and a spark of delight led us to encounter and solve many novel technical issues unique to each client platform that we support. While the Host Passport appears in the web, Android, and iOS apps, this article focuses specifically on the iOS implementation.

Implementing the Host Passport on iOS

While we’ve almost entirely switched over to SwiftUI when it comes to building new components and screens in our app, we opted to use UIKit for the passport animation. We did this for a couple of reasons. Firstly, at the time of this writing, SwiftUI does not have APIs supporting custom transitions and navigation patterns, so our screen navigation and transition layer remains in UIKit. And secondly, while keyframe timing was introduced for SwiftUI animations with iOS 17, our version support extended back to iOS 15 at the time of release.

UIKit provides a ready-to-use framework that enables the development of smooth, polished animations. Combined with our in-house declarative transition framework, we were starting with a solid foundation that we could leverage to create complex animations. The work lay in bridging the gap between established patterns and our novel requirements; while we were already experienced in creating delightful two-dimensional animations, three-dimensional animation was uncharted territory.

The complexity of this animation lies in its many moving parts. The challenge lies not in animating single properties, but rather coordinating many for a cohesive effect that is not only functional, but also delightful.

The Passport

The anchor point

The anchor point is a property of a view’s bounds, defaulting to the relative [0.5, 0.5], or exact center. Rotation animations rotate around this anchor point, so by default, views rotate around their midpoints, which gives a card rotating effect rather than a page flipping one.

A rounded rectangle rotating around a center anchor point

To achieve the desired page rotation, we faced a dilemma with the anchor point. Shifting the anchor point to [0, 0.5] in the coordinate space could accomplish the page turning effect by shifting it to the view’s leading side, but that approach had the potential to disrupt other aspects of the animation — this is because the anchor point is used not only as the basis for rotation, but for other transforms, such as scaling and translation. Altering the anchor point for three-dimensional rotation has a knock-on effect on these other transforms, causing unexpected side effects we would then have to work around.

With this in mind, we used an alternative approach: instead of directly manipulating the anchor point, we created transparent views where the visible content occupied only half of the space. As the rotation occurs, the view seemingly rotates around the left edge, while still leveraging the default center point for the actual rotation.

With this, we are able to animate our book page rotation without introducing complications to the other transforms. See the example below, where there is a border added around the entire view, including the transparent part, to show its actual size.

A rounded rectangle with a red half and a transparent half rotating around a center anchor point

Page composition

With the rotation solved, we next had to think about how to compose the view to look like a book. We ended up accomplishing that effect by using a compound view. At a basic level, the booklet is composed of a front page and two inside pages. That meant we needed three separate views:

A view that rotates like a folding page with View 1 (front), View 2 (inner left) and View 3 (inner right)

By stitching them together, we create the Passport booklet.

To create the impression of a page flip, we needed to employ another trick; while real life pages have a front and a back, the same is not true of a view. Therefore, in order to make it look like a page turning, we timed it so that during the page turn, the front view is swapped for the back view at the exact point when the page is completely orthogonal to the viewer’s perspective. This creates the illusion of a front and a back. Et voila!

A rotating rounded rectangle that changes from blue to red when it is perpendicular to the screen

Integrating with our Animation Framework

At this point, we had a passport booklet with the ability to flip open in three dimensions.

In order to accomplish the next step in the animation we needed to integrate our book animation with our declarative animation framework, which handled transitioning the animating passport from the listing results view onto the modal view. Our animation framework allows us to perform a shared element transition, where a view animates seamlessly between two separate screens, in just a few lines of code.

First, we created a transition definition that describes the type of animation we wanted:

let passportTransition: TransitionDefinition = [
SharedElementIdentifiers.Passport.passport(listingId): .sharedElement
]

Next, we attached these identifiers to the source view (the passport in the listing search results) and the destination (the open passport card in the context sheet.) We set the transition definition on the modal presentation, and from there, the framework created the animation that moved our passport view from its starting location in the listing results to its final location in the modal.

Under typical circumstances, our framework captures a “snapshot” of the view by rendering it as a static image. The snapshot is then animated from the initial position to the final position, while the original source and destination views are hidden during the animation. This allows us to play the animation of the view moving from one place to another in a performant way while keeping the view hierarchy intact.

In our case, however, a static snapshot didn’t have the functionality we needed, which was the ability to play the page flip animation alongside the shared element transition. Therefore, we created a custom snapshot that we used in place of the default static snapshot. This custom snapshot was a copy of the view that did have animation capabilities, that we then triggered to play alongside the animated transition so that they would be perfectly in sync. Enter UIViewPropertyAnimator: a class that allows us to define animation blocks and dynamically control their playback. It provides the flexibility to start, stop, or modify animations in real-time.

It neatly encapsulated our animations within a single object, which could then be passed along to our animation framework. As our framework handled the screen to screen transition, it triggered the custom animation to play in sync with that transition.

Timing

It isn’t only where a view moves that determines realism, but also very importantly when. The passport opens in the span of a moment, but the simple elegance belies the complexity underneath.

On a closer look, our animation consists of many synchronized individual animations. The passport grows in size, moves along the x and y axis, rotates its pages in 3D space, and shadows move to simulate light and movement. To get things just right, we use a separate timing curve for each property.

But we need even more specificity than that; our design calls for these to start and stop at different points along the animation duration. For that, we time specific events to relative points within the timing curve via keyframes. To expand on our earlier example, here is our animator with keyframes set.

let animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut) {

UIView.animateKeyframes(withDuration: 0, delay: 0) {

UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {

cardView.transform = CGAffineTransform(translationX: 0, y: 100) }

UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0) {

cardView.backgroundColor = .red }

UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {

cardView.transform = .identity } }}

animator.startAnimation()

Next, let’s take a closer look at spring timings and their unique characteristics. When creating animations, we have the option of different types of easing functions for a naturalistic feel.

Easing functions like linear and cubic are common timing curves, as depicted in the graphs below. They give us the ability to specify the speed of our animation over time.

Linear

- 위키
Copyright © 2011-2024 iteam. Current version is 2.137.1. UTC+08:00, 2024-11-08 23:54
浙ICP备14020137号-1 $방문자$