Разработка
Инженерия движения в масштабе
Как Airbnb применяет шаблоны декларативного проектирования для быстрого создания плавных анимаций переходов.
Движение является ключевой частью того, что делает цифровой опыт простым и приятным в использовании. Плавные переходы между состояниями и экранами являются ключом к тому, чтобы помочь пользователю сохранить контекст при перемещении по функциям. Быстрая анимация оживляет приложение и придает ему индивидуальность.
В Airbnb мы запускаем сотни функций и экспериментов, разработанных инженерами из разных команд. При разработке в таком масштабе очень важно учитывать эффективность и поддерживаемость всего нашего стека технологий, и движение не является исключением. Добавление анимации к фиче должно быть быстрым и простым. Инструментарий должен дополняться и естественным образом сочетаться с другими компонентами нашей функциональной архитектуры. Если создание анимации занимает слишком много времени или ее слишком сложно интегрировать в общую архитектуру функций, то зачастую это первая часть взаимодействия с продуктом, которая отбрасывается при переходе от дизайна к реализации.
В этом посте мы обсудим новый фреймворк для iOS, который мы создали, чтобы помочь воплотить это видение в жизнь.
Императивные переходы UIKit
Давайте рассмотрим этот переход на главной странице приложения Airbnb, который переводит пользователей из результатов поиска на расширенный экран ввода данных:
Пример перехода из приложения Airbnb для iOS — от расширения к сворачиванию экрана поиска.
Переход является ключевой частью дизайна, благодаря которому весь процесс поиска кажется цельным и легким.
В рамках традиционных шаблонов UIKit есть два способа построить подобный переход. Один из них заключается в создании единого массивного контроллера представления, который содержит как результаты поиска, так и экраны ввода данных поиска, и управляет переходом между двумя состояниями с помощью императивных блоков анимации UIView. Несмотря на то, что этот подход прост в реализации, его недостатком является тесная связь этих двух экранов, что делает их гораздо менее удобными в сопровождении и менее переносимыми.
Другой подход заключается в реализации каждого экрана как отдельного view controller-а и создании специальной реализации UIViewControllerAnimatedTransitioning, которая извлекает соответствующие представления из каждой иерархии представлений, а затем анимирует их. Обычно это сложнее реализовать, но ключевое преимущество заключается в том, что каждый отдельный экран может быть построен как отдельный UIViewController, как вы делаете и для любой другой функции.
В прошлом мы создавали переходы с использованием обоих этих подходов и обнаружили, что оба они обычно требуют сотен строк хрупкого императивного кода. Это означало, что для создания пользовательских переходов требовалось много времени и их было сложно поддерживать, поэтому они обычно не включались в основной процесс разработки функции командой.
Общей тенденцией является отход от такого императивного проектирования систем к декларативным шаблонам. Мы широко используем декларативные системы в Airbnb — мы используем такие фреймворки, как Epoxy и SwiftUI, для декларативного определения макета каждого экрана. Экраны объединяются в функции и потоки с помощью API-интерфейсов декларативной навигации. Мы обнаружили, что эти декларативные системы обеспечивают значительный прирост производительности, позволяя инженерам сосредоточиться на определении того, как должно вести себя приложение, и абстрагироваться от сложных лежащих в основе деталей реализации.
Декларативные анимации переходов
Чтобы упростить и ускорить процесс добавления переходов в наше приложение, мы разработали новый фреймворк для создания переходов декларативно, а не императивно, как делали раньше. Мы обнаружили, что этот новый подход значительно упростил создание кастомных переходов, и в результате гораздо больше инженеров смогли легко добавлять сложные и восхитительные переходы на свои экраны даже в сжатые сроки.
Чтобы выполнить переход с этим новым фреймворком, вы просто предоставляете начальное состояние и конечное состояние (или, в случае перехода экрана, контроллеры исходного и целевого представления) вместе с декларативным определением того, как должен быть анимирован каждый отдельный элемент на экране. Общая реализация UIViewControllerAnimatedTransitioning фреймворка обрабатывает все остальное автоматически.
Этот новый фреймворк стал инструментом в том, как мы создаем фичи. Он поддерживает многие новые функции, включенные в летнюю версию Airbnb 2022 года и зимнюю версию 2022 года, что делает их простыми и приятными в использовании:
Пример переходов в приложении Airbnb для iOS в новых функциях, представленных в 2022 году.
В качестве введения начнем с примера. Вот простое действие «поиск», когда выбор даты на нижнем листе скользит вверх по странице с контентом:
Пример перехода для простой функции «поиск».
В этом примере есть два отдельных контроллера представления: экран результатов поиска и экран выбора даты. Каждый из компонентов, которые мы хотим анимировать, помечен идентификатором, чтобы установить их идентичность.
Диаграмма, показывающая экран результатов поиска и экран выбора даты с аннотациями идентификаторов компонентов.
Эти идентификаторы позволяют нам обращаться к каждому компоненту семантически по имени, а не напрямую ссылаться на экземпляр UIView. Например, компонент Explore.searchNavigationBarPill на каждом экране — это отдельный экземпляр UIView, но поскольку они помечены одним и тем же идентификатором, два экземпляра представления считаются отдельными «состояниями» одного и того же компонента.
Теперь, когда мы определили какие компоненты хотим анимировать, мы можем определить как они должны анимироваться. Для этого перехода мы хотим:
- Фон будет меняться.
- Bottom Sheet будет скользить вверх от нижней части экрана.
- Панель навигации будет анимироваться между первым и вторым состоянием (анимация «общего элемента»).
Мы можем выразить это как простое определение перехода:
let transitionDefinition: TransitionDefinition = [ BottomSheet.backgroundView: .crossfade, BottomSheet.foregroundView: .edgeTranslation(.bottom), Explore.searchNavigationBarPill: .sharedElement, ]
Возвращаясь к приведенному выше примеру для расширения и свертывания экрана ввода данных для поиска, мы хотим:
- Размытие фона.
- Top bar и Bottom bar выезжают.
- Строка поиска на главном экране переходит в «where are you going?» карточку.
- Две другие карточки поиска должны скрываться, оставаясь привязанными к «куда ты идешь? карточке.
Вот как эта анимация определяется с помощью синтаксиса декларативного перехода:
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 нельзя просто скопировать, поэтому копии обычно делаются путем «моментального снимка» представления (рендеринга его в виде изображения). Мы временно скрываем «исходный view» во время воспроизведения анимации, чтобы был виден только снепшот.
После того, как фреймворк настроил иерархию представлений контейнера перехода и определил конкретную анимацию для использования для каждого компонента, анимацию просто нужно применить и воспроизвести. Именно здесь выполняются базовые императивные анимации UIView.
Вывод
Как и в случае с Epoxy и другими декларативными системами, абстрагирование базовой сложности и предоставление простого декларативного интерфейса позволяет инженерам сосредоточиться на том, что делать, а не как. Декларативное определение переходов для этих анимаций состоит всего из нескольких строк кода, что само по себе является огромным улучшением по сравнению с любой возможной императивной реализацией. А поскольку наши API-интерфейсы декларативного построения функций имеют первоклассную поддержку реализаций UIKit UIViewControllerAnimatedTransitioning, эти декларативные переходы можно интегрировать в существующие функции без внесения каких-либо изменений в архитектуру. Это значительно ускоряет разработку функций, упрощая создание отточенных переходов, а также обеспечивая гибкость и удобство сопровождения в долгосрочной перспективе.
У нас впереди насыщенная дорожная карта. Одним из направлений активной работы является улучшение взаимодействия со SwiftUI. Это позволяет нам плавно переходить между экранами на основе UIKit и SwiftUI, что открывает нам возможность постепенного внедрения SwiftUI в нашем приложении без необходимости жертвовать движением. Мы также изучаем возможность сделать аналогичные фреймворки доступными в Интернете и на Android. Наша долгосрочная цель — максимально упростить воплощение замечательных идей нашего дизайнера в реальные продукты для всех платформ.