Connect with us

Разработка

Как Airbnb внедрила SwiftUI в приложение для iOS

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

Опубликовано

/

     
     

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

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

В 2019 году ландшафт iOS UI-фреймворков изменился с появлением SwiftUI — “фирменного” декларативного UI-фреймворка, который решает многие из тех же задач, что и Epoxy. Хотя в течение первых трех лет SwiftUI не очень подходил для наших нужд, к 2022 году он стал более стабильным и функциональным в своих API. Примерно в это время мы начали рассматривать возможность внедрения SwiftUI в Airbnb.

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

Оценка и планирование для внедрения SwiftUI

Переход на новый UI-фреймворк — это не та задача, к которой следует относиться легкомысленно. После долгих исследований мы пришли к выводу, что SwiftUI не приведет к регрессу пользовательского опыта и улучшит опыт разработчиков благодаря следующим гипотезам:

  • Гибкость и композитность: SwiftUI предложит более мощные и гибкие паттерны для управления вариантами представлений и их стилизацией, а также общие представления и модификаторы представлений. Это должно существенно сократить количество View, необходимых для создания приложения, поскольку будет проще как настраивать существующие представления, так и компоновать новое поведение в месте вызова.
  • Полная декларативность: Код SwiftUI будет более простым в написании и изменении с течением времени. Как правило, не должно быть контекстного переключения между императивной и декларативной парадигмами кодирования, как это было в Epoxy, в котором инженерам часто приходилось «спускаться» в код UIKit.
  • Меньше кода: В результате того, что SwiftUI полностью декларативен, мы полагали, что для создания компонента представления SwiftUI потребуется значительно меньше кода. Как правило, количество ошибок коррелирует с количеством строк кода.
  • Более быстрая итерация: Предварительные просмотры в Xcode позволят практически мгновенно выполнять циклы итерации компонентов представления и экранов SwiftUI, по сравнению с 30-секундными и более циклами сборки и запуска в UIKit.
  • Идиоматичность: SwiftUI снизит когнитивные издержки при создании пользовательского интерфейса за счет уменьшения количества пользовательских парадигм и паттернов. Это упростит прием на работу новых инженеров.

Исходя из этих гипотез, мы разработали план оценки и внедрения SwiftUI, состоящий из трех этапов:

  • Этап 1: Создание побочных представлений, таких как компоненты многоразового использования, на основе нашей системы проектирования.
  • Этап 2: Создание целых экранов, таких как страница с информацией о бронировании или страница с профилем пользователя.
  • Этап 3: Создание полных фич, состоящих из нескольких экранов.

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

Включение SwiftUI

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

Дизайн-система

Первоклассная поддержка SwiftUI для дизайн-системы Airbnb была ключевым приоритетом для ускорения внедрения SwiftUI в масштабах всей компании. Вместо того чтобы просто перенести ее с существующих компонентов UIKit, мы перестроили дизайн-систему для SwiftUI, сделав ее гораздо более гибкой и мощной.

Каждый компонент представления в нашей системе дизайна поддерживает стилизацию, что позволяет повысить удобство повторного использования и настройки. У нас есть ряд протоколов стилей, которые в сочетании с генерируемым кодом позволяют передавать объекты стилей вниз через среду SwiftUI, чтобы имитировать встроенные парадигмы стилей SwiftUI. Один из типов стилей, соответствующих этому протоколу, называется «гибкими стилями» (flexible styles). Приведем пример кода:

public protocol FlexibleSwiftUIViewStyle: DynamicProperty {
  /// The content view type of this style, passed to `body()`.
  associatedtype Content
  /// The type of view representing the body.
  associatedtype Body: View
  /// Renders a view for this style.
  @ViewBuilder
  func body(content: Content) -> Body
}

Этот протокол позволяет создать стилизованный объект с набором настраиваемых свойств, которые могут полностью определить визуализацию компонента. Стилю передается объект content, чтобы он мог получить доступ к базовому состоянию представления или взаимодействию при создании нового тела представления. Ниже приведен пример реализации стиля для числового степпера (для краткости некоторые элементы стиля опущены):

public struct DefaultStepperStyle: DLSNumericStepperStyle {
  public var valueLabel = TextStyle…


  public func body(content: DLSNumericStepperStyleContent) -> some View {
    HStack {
      Button(action: content.onDecrement) { subtractIcon }
        .disabled(content.atLowerBound)
      Text(content.description)
        .textStyle(valueLabel)
      Button(action: content.onIncrement) { addIcon }
        .disabled(content.atUpperBound)
    }
  }
}
Как Airbnb внедрила SwiftUI в приложение для iOS

Пример степпера, созданного на основе свойств стиля по умолчанию

Однако с помощью гибких стилей инженеры могут добавить совершенно собственный стиль степпера всего за несколько десятков строк кода, реализовав новый тип, соответствующий DLSNumericStepperStyle. Этот стиль может быть установлен в представлении с помощью автогенерируемого модификатора представления:

DLSNumericStepper(value: $value, in: 0...)
  .dlsNumericStepperStyle(CustomStepperStyle())
Как Airbnb внедрила SwiftUI в приложение для iOS

Пример степпера, созданного на основе свойств пользовательского стиля.

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

Бридж в Epoxy

Epoxy обеспечивает работу тысяч экранов в приложении Airbnb. Чтобы обеспечить беспрепятственное внедрение SwiftUI, мы создали инфраструктуру, позволяющую Epoxy не только подключать представления SwiftUI к спискам Epoxy на базе UIKit, но и подключать представления UIKit Epoxy к SwiftUI.

Для подключения представлений SwiftUI к списку UIKit Epoxy мы создали модификатор представления itemModel, который устанавливает идентификатор Epoxy для представления SwiftUI. В реализации этот метод оборачивает представление в UIHostingController и встраивает его в ячейку представления коллекции. Эта утилита открыла первый этап внедрения SwiftUI, сделав простым внедрение SwiftUI в существующие экраны Epoxy.

SwiftUIRow(
  title: "Row \(id)",
  subtitle: "Subtitle")
  .itemModel(dataID: id)

Аналогичным образом можно прокинуть представления UIKit в SwiftUI с помощью расширения, которое создает представление SwiftUI из компонента UIKit, используя его содержимое, стилевые инварианты и любую дополнительную конфигурацию представления. В реализации этот API использует общий UIViewRepresentable, который автоматически создает и обновляет UIView по мере изменения его содержимого и стиля.

EpoxyRow.swiftUIView(
  content: .init(title: "Row \(index)", subtitle: …),
  style: .small)
  .configure { context in
    print("Configuring \(context.view)")
  }
  .onTapGesture {
    print("Row \(index) tapped!")
  }

Учитывая значительно отличающуюся систему компоновки SwiftUI, правильная компоновка компонента UIKit представляла собой сложную задачу. Мы разработали настраиваемый подход, который автоматически поддерживает сложные представления, такие как UILabel, для правильного размещения которых требуется дополнительный проход компоновки.

Однонаправленный поток данных

В Epoxy мы обнаружили, что использование однонаправленного потока данных делает наш пользовательский интерфейс предсказуемым и удобным для понимания. Мы построили наши экраны таким образом, чтобы содержимое Epoxy отображалось в зависимости от состояния экрана. Взаимодействие с пользователем происходит в виде действий, которые приводят к изменению состояния и вызывают повторный рендеринг экрана. Для хранения состояния экрана и обработки действий по изменению этого состояния мы используем объект StateStore. Чтобы адаптировать этот паттерн к SwiftUI, мы обновили StateStore и привели его в соответствие с ObservableObject, что позволяет хранилищу вызывать повторный рендеринг SwiftUI экранов при изменении состояния. Мы обнаружили, что инженеры предпочитают продолжать создавать экраны в SwiftUI, используя этот подход, поскольку он позволяет отделить бизнес-логику и логику изменения состояния от логики представления. Во многих случаях мы смогли перенести логику экранов из Epoxy в экраны SwiftUI без каких-либо изменений. Чтобы проиллюстрировать сходство, приведем простой экран счетчика, реализованный в обеих системах представления:

// In Epoxy/UIKit:
struct CounterContentPresenter: StateStoreContentPresenter {
  let store: StateStore<CounterState, CounterAction>


  var content: UniListViewControllerContent {
    .currentDLSStandardStyle()
    .items {
      BasicRow.itemModel(
        dataID: ItemID.count,
        content: .init(titleText: "Count \(state.count)"),
        style: .standard)
        .didSelect { _ in
          store.handle(.increment)
        }
    }
  }
}
// In SwiftUI
struct CounterScreen: View {
  @ObservedObject 
  let store: StateStore<CounterState, CounterAction>


  var body: some View {
    DLSListScreen {
      DLSRow(title: "Count \(store.state.count)")
        .highlightEffectButton {
          store.handle(.increment)
        }
    }
  }
}

Тестируемость

Для обеспечения высокого качества продукта мы стремились к тому, чтобы код SwiftUI был тестируемым. Основным подходом к тестированию представлений является Snapshot-тестирование, поэтому мы используем статическое определение для предоставления именованных вариантов представлений как браузеру компонентов, так и службе снепшот тестирования:

enum DLSPrimaryButton_Definition: ViewDefinition, PreviewProvider {
  static var contentVariants: ContentVariants {
    DLSPrimaryButton(title: "Title") { … }
      .named("Short text")


    DLSPrimaryButton(title: "Title") { … }
      .disabled(true)
      .named("Disabled")
  }
}

Поскольку в данном случае мы возвращаем варианты представлений, это дает большую свободу действий при тестировании — фреймворк принимает любые варианты содержимого или комбинации модификаторов представлений. Кроме того, мы приводим эти определения в соответствие с протоколом PreviewProvider в SwiftUI и преобразуем эти варианты содержимого в ожидаемый возвращаемый тип, чтобы инженеры могли быстро проверить компонент с помощью Xcode Previews.

В отличие от декларативных UI-фреймворков на других платформах, SwiftUI не предоставляет встроенной библиотеки тестирования. Для поддержки тестов компонентов и экранов в поведенческом стиле мы интегрировали библиотеку ViewInspector с открытым исходным кодом, в создание которой мы также внесли свой вклад.

Образование

От некоторых наших коллег мы узнали, что существенной проблемой при внедрении SwiftUI является накопление собственного опыта в большой команде iOS-разработчиков. Чтобы решить эту проблему, мы провели несколько полунедельных семинаров по основам SwiftUI, в которых приняла участие почти половина нашей команды iOS-инженеров. По словам участников, их уверенность в основах SwiftUI выросла на 37%, а уверенность в создании новых компонентов — на 39%. Кроме того, спустя почти год участники семинара отметили, что их знания SwiftUI на 8% выше, чем у тех, кто не посещал семинары.

Выводы о внедрении SwiftUI

Строки кода

Учитывая огромную кодовую базу компании Airbnb, мы были рады возможности SwiftUI сократить количество кода, необходимого для создания пользовательского интерфейса. В ходе раннего эксперимента, в ходе которого мы переписали карточку отзыва, мы увидели 6-кратное сокращение количества строк кода — с 1121 до всего лишь 174 строк! В течение последних двух лет мы наблюдали аналогичное сокращение количества строк кода по мере внедрения SwiftUI.

Производительность

Производительность пользовательского интерфейса была одним из ключевых моментов при оценке SwiftUI. К счастью, после проведения многочисленных экспериментов мы убедились, что производительность страницы при использовании SwiftUI сопоставима с реализацией UIKit. Мы заметили небольшие накладные расходы при инстанцировании UIHostingController, но смогли снизить их за счет добавления в Epoxy пула повторного используемых хостинг-контроллеров.

Принятие и удовлетворенность разработчиков

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

По состоянию на сентябрь мы имеем более 500 View на SwiftUI и около 200 экранов на SwiftUI. Многие экраны для летнего релиза Airbnb 2023 года были полностью построены на SwiftUI.

Как Airbnb внедрила SwiftUI в приложение для iOS

Рост числа компонентов и экранов на SwiftUI в продукте Airbnb

Инженеры Airbnb, работающие с iOS, также очень довольны SwiftUI. В ходе нашего последнего опроса 77% респондентов заявили, что SwiftUI повышает эффективность их работы. Многие респонденты отметили, что их эффективность еще больше повысится с увеличением опыта работы с SwiftUI, в том числе так сказали и те, кто оценил его как замедляющий их работу. 100% респондентов заявили, что SwiftUI не оказывает негативного влияния на качество их фич, а некоторые отметили, что SwiftUI улучшает качества их кода.

Проблемы

Несмотря на то что переход на SwiftUI в целом был успешным, мы столкнулись со следующими проблемами:

  • В то время как Swift и окружающая его основа были открыты, реализация SwiftUI остается «черным ящиком». Если бы SwiftUI был открыт, мы могли бы лучше понять фреймворк и эффективнее проводить отладку.
  • Наша информация о развитии SwiftUI ограничивается ежегодными анонсами. Если бы у нас было более четкое понимание того, куда движется SwiftUI, мы могли бы лучше определить приоритеты внедрения и понять, куда вкладывать деньги в собственной компании.
  • Airbnb поддерживает две последние версии iOS. Если бы новые API SwiftUI были перенесены на старые версии iOS, мы могли бы быстрее воспользоваться новыми мощными функциями и тратить меньше времени на написание запасных решений.
  • Чтобы полностью отказаться от UIKit, нам понадобится набор API-интерфейсов SwiftUI, поддерживающих пользовательские переходы и схемы навигации.
  • При использовании LazyVStack и ScrollView мы столкнулись с рядом проблем и ограничений, в том числе:
    • Анимации вставки, удаления и обновления часто не работают.
    • Предварительная выборка ячеек за пределами экрана и предварительная выборка изображений или данных невозможна.
    • Некоторые состояния сбрасываются при прокрутке за пределы экрана.
  • API-интерфейсы SwiftUI для ввода текста поддерживают не все функции, которые поддерживаются их аналогами в UIKit, поэтому инженерам приходится переходить на UIKit.
  • У нас есть 18 открытых отзывов в Apple, которые документируют ошибки и возможные улучшения SwiftUI, с которыми мы столкнулись.

Заключение

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

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Хорошие новости

Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: