Как Airbnb применяет декларативные шаблоны проектирования для быстрого создания плавной анимации перехода.

Автор: Кэл Стивенс

Движение является ключевой частью того, что делает цифровой опыт простым и восхитительным в использовании. Плавные переходы между состояниями и экранами - это ключевой элемент, помогающий пользователю сохранить контекст при навигации по функционалу. Быстрые всплески анимации оживляют приложение и помогают придать ему особую индивидуальность.

В Airbnb мы запускаем и проводим сотни функций и экспериментов, разработанных инженерами многих команд. При построении в таком масштабе очень важно учитывать эффективность и ремонтопригодность во всем нашем технологическом стеке, и движение не является исключением.

Добавление анимации к элементу должно быть быстрым и простым. Инструментарий должен дополнять и естественным образом соответствовать другим компонентам нашей функциональной архитектуры. Если анимация занимает слишком много времени при сборке или её слишком сложно интегрировать в общую функциональную архитектуру, - то зачастую она становится первым из решений во взаимодействии с продуктом, которое отбрасывается при переводе от проектирования к реализации.

В этом посте мы обсудим новую структуру для iOS, которую мы создали, чтобы помочь воплотить это видение в реальность.

Императивные Переходы UIKit

Рассмотрим этот переход на домашней странице приложения Airbnb, которая переносит пользователей из результатов поиска на расширенный экран с вводом поиска:

Пример перехода: раскрытие и сворачивание экрана ввода поиска в приложении Airbnb для iOS
Пример перехода: раскрытие и сворачивание экрана ввода поиска в приложении Airbnb для iOS

Переход является ключевой частью дизайна, благодаря которому весь процесс поиска кажется цельным и легким.

В рамках традиционных шаблонов UIKit есть два способа создать подобный переход. Один из них заключается в создании единого массивного вью-контроллера, который содержит как экран с результатами поиска, так и экран ввода поиска, и управляет переходом между этими двумя состояниями с помощью императивных блоков анимации  UIView. Несмотря на то, что этот подход прост в реализации, его недостатком является тесная связь этих двух экранов, что делает их гораздо менее ремонтопригодными и портативными.

Другой подход заключается в реализации каждого экрана как отдельного вью-контроллера и создании специальной реализации UIViewControllerAnimatedTransitioning, которая извлекает соответствующие вью из каждой иерархии вью, а затем анимирует их. Обычно это сложнее реализовать, но ключевое преимущество заключается в том, что каждый отдельный экран может быть построен как отдельный UIViewController, как и для любой другой функции.

В прошлом мы создавали переходы с использованием обоих этих подходов и обнаружили, что оба они обычно требуют сотен строк хрупкого императивного кода. Это означало, что для создания пользовательских переходов требовалось много времени и их было сложно поддерживать, поэтому обычно они не включались в основной процесс разработки функций.

Общей тенденцией стал отход от такого рода императивного проектирования систем в сторону  декларативных паттернов. В Airbnb мы широко используем декларативные системы - мы применяем такие фреймворки, как Epoxy и SwiftUI, для декларативного определения макета каждого экрана. Экраны объединяются в функции и потоки с помощью декларативных навигационных API. Мы обнаружили, что такие декларативные системы значительно повышают производительность, позволяя инженерам сосредоточиться на определении того, как должно вести себя приложение, и абстрагироваться от сложных деталей реализации, лежащих в основе.

Декларативные Анимации Перехода

Чтобы упростить и ускорить процесс добавления переходов в наше приложение, мы разработали новый фреймворк для создания переходов декларативно, а не императивно, как делали раньше. Мы обнаружили, что данный подход значительно упростил создание нестандартных переходов; как  результат -  гораздо больше инженеров смогли легко добавлять насыщенные и восхитительные переходы на свои экраны даже в сжатые сроки.

Чтобы выполнить переход с помощью этого фреймворка, вы просто предоставляете начальное состояние и конечное состояние (или, как в случае экранного перехода, - исходный и целевой вью-контроллеры) вместе с декларативным определением перехода того, как должен быть каждый отдельный элемент на экране должен быть анимирован. Общая реализация UIViewControllerAnimatedTransitioning в данном фреймворке обрабатывает все остальное автоматически.

Этот новый фреймворк сыграл важную роль в том, как мы создаем функции. Он поддерживает многие новые функции, включенные в Летний Релиз 2022 и Зимний Релиз 2022 от Airbnb, что делает их простыми и приятными в использовании:

Примеры переходов в приложении Airbnb для iOS от новых функций, представленных в 2022 году
Примеры переходов в приложении Airbnb для iOS от новых функций, представленных в 2022 году

В качестве введения начнем с примера. Здесь простое взаимодействие с «поиском», где панель выбора даты скользит снизу вверх по странице с контентом:

В этом примере есть два отдельных вью-контроллера: экран с результатами поиска и экран выбора даты. Каждый из компонентов, которые мы хотим анимировать, помечен идентификатором, чтобы определять их идентичность.

Диаграмма, показывающая экран результатов поиска и экран выбора даты с аннотациями идентификаторов компонентов
Диаграмма, показывающая экран результатов поиска и экран выбора даты с аннотациями идентификаторов компонентов

Эти идентификаторы позволяют нам обращаться к каждому компоненту семантически по имени, а не напрямую ссылаться на экземпляр UIView. Например, компонент Explore.searchNavigationBarPill на каждом экране - это отдельный экземпляр UIView, но поскольку они помечены одним и тем же идентификатором, эти два экземпляра вью считаются отдельными «состояниями» одного и того же компонента.

Теперь, когда мы определили компоненты, которые хотим анимировать, мы можем определить, как это должно происходить. Для этого перехода мы хотим чтобы:

  1. Фон затухал.

  2. Нижняя панель скользила вверх от нижней части экрана.

  3. Панель навигации анимировалась между первым и вторым состоянием (анимация «общего элемента»).

Мы можем выразить это как простое определение перехода:

let transitionDefinition: TransitionDefinition = [
  BottomSheet.backgroundView: .crossfade,
  BottomSheet.foregroundView: .edgeTranslation(.bottom),
  Explore.searchNavigationBarPill: .sharedElement,
]

Возвращаясь к приведенному выше примеру с раскрытием и сворачиванием экрана ввода поиска, мы хотим чтобы:

  1. Фон размывался.

  2. Верхняя планка и нижняя планка сдвигались вовнутрь.

  3. Строка поиска на главном экране переходила в карточку «Куда вы отправляетесь?»

  4. Две другие карточки поиска исчезали, оставаясь тематически привязанными к карточке «Куда вы отправляетесь?

Вот как эта анимация определяется с помощью синтаксиса определения декларативного перехода:

let transitionDefinition: TransitionDefinition = [
  SearchInput.background: .blur,
  SearchInput.topBar: .translateY(-40),
  SearchInput.bottomBar: .edgeTranslation(.bottom),
  
  SearchInput.whereCard: .sharedElement,
  SearchInput.whereCardContent: .crossfade,
  SearchInput.searchInput: .crossfade,
  
  SearchInput.whenCard: .anchorTranslation(relativeTo: SearchInput.whereCard),
  SearchInput.whoCard: .anchorTranslation(relativeTo: SearchInput.whereCard),
]

Как это работает

Этот API определения декларативного перехода является мощным и гибким, но это только половина истории. Для реального выполнения анимации наша структура предоставляет общую реализацию UIViewControllerAnimatedTransitioning, которая принимает определение перехода и управляет анимацией перехода. Чтобы выяснить, как работает эта реализация, мы вернемся к простому взаимодействию «поиск».

Во-первых, фреймворк проходит по иерархии вью как исходного, так и целевого экранов, чтобы извлечь UIView для каждого из анимированных идентификаторов. Это определяет, присутствует ли данный идентификатор на каждом экране, и формирует иерархию идентификаторов (схожую с  иерархией вью экрана).

«Иерархия идентификаторов» исходного и целевого экранов
«Иерархия идентификаторов» исходного и целевого экранов

Иерархии идентификаторов источника и цели различаются, чтобы определить, был ли отдельный компонент добавлен, удален или присутствует в них обоих. Если вью было добавлено или удалено, фреймворк будет использовать анимацию, указанную в определении перехода. Если вью присутствовало в обоих состояниях, то фреймворк уже выполнит «анимацию общего элемента», - т.е.  компонент анимируется из своего исходного положения в конечное положение, пока его содержимое обновляется. Эти общие элементы анимируются рекурсивно - каждый компонент может предоставлять свою собственную иерархию идентификаторов дочерних элементов, которые также меняются и анимируются.

Окончательная иерархия идентификаторов после сравнения исходного и целевого экранов.
Окончательная иерархия идентификаторов после сравнения исходного и целевого экранов.


Чтобы действительно выполнить эти анимации, нам нужна единая иерархия вью, которая соответствует структуре нашей иерархии идентификаторов. Мы не можем просто объединить исходный и целевой экраны в единую иерархию вью, наложив их друг на друга, потому что порядок будет неправильным. В этом случае, если бы мы просто разместили целевой экран поверх исходного экрана, то исходное вью Explore.searchNavigationBarPill было бы ниже целевого элемента BottomSheet.backgroundView, который не соответствует иерархии идентификаторов.

Вместо этого мы должны создать отдельную иерархию вью, соответствующую структуре иерархии идентификаторов. Для этого нужно создать копии анимируемых компонентов и добавить их в контейнер перехода UIKit. Большинство UIView нельзя просто скопировать, поэтому копии обычно делаются путем «моментального снимка» вью (его рендеринга как изображения). Во время воспроизведения анимации мы временно скрываем «оригинальный вид», чтобы был виден только снимок.

После того, как фреймворк настроил иерархию вью контейнера перехода и определил для использования конкретную анимацию для каждого компонента, остается только применить и воспроизвести её. Именно здесь выполняются базовые императивные анимации UIView.

Вывод

Как и в случае с Epoxy и другими декларативными системами, абстрагирование базовой сложности и предоставление простого декларативного интерфейса позволяет инженерам сосредоточиться на вопросе: что, а не как.

Декларативное определение перехода для этих анимаций состоит всего из нескольких строк кода, что само по себе является огромным улучшением по сравнению с любой возможной императивной реализацией. А поскольку наши API декларативного построения функций имеют первоклассную поддержку реализаций UIKit UIViewControllerAnimatedTransitioning, эти декларативные переходы можно интегрировать в существующие функции без внесения каких-либо изменений в архитектуру. Это значительно ускоряет разработку функций, позволяя как никогда легко создавать первоклассные переходы, одновременно обеспечивая гибкость и ремонтопригодность в долгосрочной перспективе.

У нас впереди насыщенная дорожная карта. Одним из направлений активной работы является улучшение совместимости со SwiftUI. Что позволит нам плавно переходить между экранами на основе UIKit и SwiftUI, открывая постепенное внедрение SwiftUI в нашем приложении без необходимости жертвовать движением. Мы также изучаем возможность сделать аналогичные фреймворки доступными в web и на Android.

Наша долгосрочная цель - максимально упростить воплощение замечательных идей нашего дизайнера в реальные продукты для всех платформ.

Хотите работать в Airbnb? Ознакомьтесь с этими вакансиями:

Штатный инженер-программистWishlists
Штатный инженер-программистGuests & Hosts
Штатный Android инженер-программистGuest

Благодарность

Большое спасибо Эрику Хорачеку и Мэтью Чеоку за их большой вклад в архитектуру движения Airbnb и нашу декларативную структуру перехода.

Все названия продуктов, логотипы и торговые марки являются собственностью их соответствующих владельцев. Все названия компаний, продуктов и услуг, используемые на этом веб-сайте, предназначены только в цели идентификации. Использование этих названий, логотипов и торговых марок не подразумевает их одобрения.

Комментарии (0)