Доклад: SwiftUI In A Nutshell / Тёма Пстыго (Авито)
Введение в Swift UI
Приветствие и представление темы
- Всем привет, меня зовут Миша. Мы начинаем пятнадцатый сезон Подлодки AOSCW, посвященный Swift UI.
- Прошло много времени с момента последнего сезона по Swift UI, фреймворк активно развивается и появляются новые таргеты.
- В этом сезоне мы сосредоточимся на практическом использовании Swift UI в продакшене, делясь опытом спикеров.
Представление участников
- Михаил Харитончик представляет себя и Александра Андрюхина, который также участвует в обсуждении.
- Тёма Пстыга из компании Авито расскажет о своем опыте работы с Swift UI и его развитии.
Основные концепции Swift UI
Декларативный подход
- Обсуждение базовых концепций Swift UI: как перестроить мышление от императивного к декларативному подходу.
- В отличие от UIKit, где акцент на событиях (тапы, перемещения), в Swift UI мы думаем о состоянии интерфейса как функции от состояния модели.
Сравнение с теорией графов
- В UIKit мы рассматриваем переходы между состояниями (рёбра графа), тогда как в Swift UI акцент на самих состояниях (вершины графа).
- Это упрощает логику разработки: вместо n² возможных переходов у нас n состояний.
Практические примеры использования
Пример реализации интерфейса
- В UIKit необходимо вручную пересчитывать layout; в Swift UI мы просто объявляем желаемый результат.
- Декларативный стиль позволяет избежать ручной настройки фреймов и анимаций — это делает разработку более интуитивной.
Двусторонняя привязка данных
- Пример с текстовым полем: данные движутся как от пользователя к модели, так и обратно.
- В UIKit требуется явное управление механизмами передачи данных; в Swift UI это происходит автоматически через привязку состояния.
Как работает двусторонний бандинг в SwiftUI?
Декларативный подход и бандинг
- В SwiftUI используется декларативный подход, который включает механизм бандинга — двусторонний канал связи между моделью и пользовательским интерфейсом (UI).
- Бандинг позволяет автоматически обновлять UI при изменении данных в модели и наоборот, что исключает необходимость в дополнительных механизмах синхронизации.
Концепция Identity в SwiftUI
- Обсуждается концепция Identity, которая помогает понять, как SwiftUI управляет сложными представлениями.
- Пример с логотипом подлодки демонстрирует, как изменение состояния (offset) влияет на отрисовку UI.
Анимации и перерисовка
- При изменении значения offset происходит повторная отрисовка body view, что приводит к перемещению логотипа по экрану.
- Использование модификатора анимации позволяет сделать переходы более плавными, хотя механизм анимации может быть неочевиден.
Иерархия объектов и их идентификация
- Каждый объект в UI имеет уникальный адрес памяти (Object Identifier), что дает ему жизненный цикл.
- Структуры не имеют конкретного адреса памяти; вместо этого SwiftUI использует типы представлений для идентификации объектов во время компиляции.
Непрозрачные типы и их использование
- Непрозрачные типы (opaque types), используемые в SwiftUI, скрывают конкретные реализации типов от разработчика.
- На примере простого вертикального стека показано, как компилятор обрабатывает различные уровни вложенности представлений.
Генерация типов во время компиляции
- Компилятор фиксирует всю иерархию представлений на этапе компиляции, создавая структуру для последующей отрисовки.
- Применение различных модификаторов к элементам управления меняет их родительские структуры и генерирует новые типы.
Заключение о структуре представлений
- В результате применения различных модификаторов формируется сложная структура представлений с четко определенной иерархией.
Структура и анимация в SwiftUI
Понимание фреймов и их свойств
- Фреймы в SwiftUI имеют фиксированные свойства, такие как
alignment, которые задаются на этапе компиляции. Изменение этих свойств не влияет на структуру фрейма.
- При изменении
alignmentструктура остается неизменной, что означает, что идентичность элементов также сохраняется.
Работа со списками в SwiftUI
- В некоторых случаях необходимо предоставить дополнительную информацию о списках элементам, так как их размер и содержание неизвестны на этапе компиляции.
- Пример с использованием списка: первый элемент может быть статичным (например, заголовком), для которого не требуется указывать идентификатор.
Идентификация элементов списка
- Для остальных элементов списка необходимо указать уникальные идентификаторы, чтобы избежать проблем с анимацией при изменении данных.
- Использование
selfв качестве идентификатора может привести к неправильной анимации из-за изменения хэша элемента при обновлении его полей.
Проблемы с анимацией при использовании self как ID
- Если использовать
selfкак ID, происходит полный релоад коллекции вместо ожидаемой анимации перемещения элементов.
- Это может вызвать нежелательные эффекты, если применяются модификаторы API или события жизненного цикла.
Рекомендации по использованию уникальных идентификаторов
- Лучшим решением будет использование стабильных идентификаторов от сервера для обеспечения уникальности и предотвращения конфликтов во время выполнения приложения.
- Указание явного ID помогает избежать проблем с дублированием данных и улучшает стабильность приложения.
Анимация переходов между состояниями
- Пример использования анимаций для переключения между состояниями (например, солнце и луна). Транзишены позволяют создать плавные визуальные эффекты при изменении состояния.
Проблемы с применением transition в SwiftUI
Понимание работы transition
- Транзиция не применяется к вьюхам, которые уже находятся на экране, так как они не покидают иерархию. Применение транзиции происходит только при входе или выходе вьюхи из иерархии.
- Если явно указать модификатор ID для разных состояний, это позволит применять транзицию, даже если визуально текст остается тем же.
Лейаут и его особенности
- Обсуждение лейаута начинается с примера приложения для подлодки. Дизайнер требует добавить паддинг к логотипу на сплешскрине.
- В SwiftUI есть встроенный паддинг, но разработчик решает создать кастомный паддинг для лучшего понимания работы лейаута.
Реализация кастомного паддинга
- Для реализации кастомного паддинга необходимо использовать протокол layout и реализовать два метода:
sizeThatFitsиplaceSubviews.
- Метод
sizeThatFitsдолжен возвращать размер лейаута с учетом предложенных ограничений по размеру и сабвьюхам внутри него.
Логика работы методов
- В методе
sizeThatFitsиспользуется guard для обработки только первой сабвьюхи. Это упрощает применение паддинга.
- Предложенный размер уменьшается на величину инсетов (паддинга), чтобы правильно разместить сабвьюху внутри лейаута.
Работа с proposed view size
- Метод
adjustedProposalпроверяет наличие ширины и высоты у предложенного размера. Если они отсутствуют, система позволяет занять пространство по желанию.
- Метод
placeSubviewsиспользует скорректированный размер для финальной отрисовки вьюшек, обеспечивая правильное размещение элементов внутри контейнера.
Кооперативность лейаута и работа с Geometry Reader
Основные концепции кооперативности лейаута
- Важно понимать, что мы не можем задать положение и размер сабвьюхи напрямую. Мы можем лишь указать её положение через метод
place at, а размер будет определяться самой сабвьюхой на основе переданного пропозла.
- Кооперативность проявляется в том, что супервьюха может предложить параметры, но окончательное решение о размере принимает сама сабвьюха.
- При отладке возникла проблема с расположением логотипа: он сместился вверх экрана. Это было связано с неправильным пониманием координатной системы.
- Оказалось, что
Bounс— это прямоугольник в пространстве родителя, и егоOriginне всегда равен нулю. Это важно учитывать при работе с позиционированием элементов.
- Для решения проблемы необходимо скорректировать
OriginуBounce, используя точку Inset Leading Inset Stop.
Использование Geometry Reader
- Geometry Reader позволяет определять контент как функцию от своего размера, что полезно для сложных математических расчетов при создании лейаута.
- Он удобен для случаев, когда размеры элементов строго ограничены по высоте или ширине. Например, если дизайнер задаёт фиксированную высоту ячеек.
- Geometry Reader можно использовать как оверлей или фон для других вьюх; он занимает пространство в соответствии с размерами применяемой вьюхи.
- В редких случаях Geometry Reader может занять всё оставшееся пространство после фиксированных элементов (например, заголовка).
- Не рекомендуется использовать Geometry Reader, когда размер всей вьюхи зависит от контента внутри него; это может привести к проблемам с вычислением размеров.
Проблемы и ограничения использования Geometry Reader
- Если текст должен занимать половину доступного пространства, использование Geometry Reader может быть затруднительным из-за нелинейного поведения текста по ширине и высоте.
- Текст будет изменять свои размеры: уменьшение ширины приведет к увеличению высоты и наоборот. Это создает проблему "курицы и яйца".
- Невозможно сделать так, чтобы размер текста определял размер Geometry Reader одновременно; это требует тщательного планирования структуры интерфейса.
- Важно учитывать эти ограничения при проектировании интерфейсов для обеспечения корректного отображения контента без неожиданных изменений размеров.
Конфигурация вьюх в Swift
Основные способы конфигурирования
- В UIKit существует три основных способа конфигурировать вьюхи: аргументы конструктора, мутабельные свойства и паттерн делегата для сложного поведения.
- Аргументы конструктора используются для фиксированных параметров, таких как текст или цвет текста, а также для изменяемых свойств.
Конструкторы в Swift
- В Swift конструкторы занимают более важное место по сравнению с UIKit. Первый способ — это использование конструкторов для типизации поведения компонентов.
- Примером может служить компонент на Авито, который имеет различные режимы выбора элементов (выбор нескольких, одного или ноль).
Типизация поведения через конструкторы
- Выбор конструктора определяет поведение всей вьюхи. Например, если используется конструктор для выбора одного элемента, то состояние "выбран несколько" невозможно.
- Это позволяет избежать создания дополнительных полей состояния и упрощает логику работы с компонентами.
Разделение непересекающегося поведения
- Второй пример использования конструкторов — разделение непересекающегося поведения. Например, circular progress bar имеет четыре режима отображения.
- Каждый режим реализован через отдельный конструктор с уникальной сигнатурой, что документирует юзкейсы компонента.
Гибкая кастомизация компонентов
- Конструкторы могут использоваться как инструмент гибкой кастомизации компонентов. Это позволяет избежать необходимости расширять существующие компоненты при изменении дизайна.
- Примером является стандартный компонент toggle с двумя основными конструкторами: один принимает строку, другой — абстрактный view builder.
Модификация через окружение
- Вторая группа модификаций — это конфигурация через окружение (environment). Это словарь параметров дерева вьюх.
- Изменения в корне поддерева передаются всем дочерним элементам по наследству, что удобно для массового изменения параметров группы вьюх.
Применение Environment в SwiftUI
Примеры использования
- Обсуждение необходимости изменения компонентов и примеры их применения, включая адаптацию вьюшки с солнышком и луной к цветовой схеме приложения (тёмная/светлая тема).
- В нижней части структуры используется одна и та же вьюха, где переопределён environment, что влияет на все дочерние элементы.
Передача стилей через Environment
- Применение модификатора button style к конкретной кнопке, который передаётся всем дочерним элементам через родительский View.
- Упрощение работы с состоянием загрузки для трёх компонентов без добавления аргументов во все конструкторы. Использование environment для переиспользуемого поведения.
Интуитивность использования
- Модификатор устанавливает состояние загрузки в environment, что позволяет интуитивно понимать его применение ко всем компонентам, которые могут загружаться.
Паттерн Builder в SwiftUI
Основные характеристики
- Паттерн Builder не отличается от предыдущих примеров с environment, но имеет свои детали. Он позволяет создавать методы на структурах для возврата копий этих структур.
- Чейнинг модификаторов возможен благодаря возвращаемому типу self, что упрощает создание сложных интерфейсов.
Ограничения и преимущества
- Скоуп модификатора ограничивается конкретным типом View. Это удобно при наличии множества точек кастомизации без добавления дополнительных аргументов.
Пример Tab Group с использованием Builder
Настройка табов
- Компонент Tab Group отображает скролящуюся полосу с табами. Каждый таб может иметь свой собственный параметр настройки counters.
Причины выбора Builder
- Counters являются специфичными для каждого Tab Group и не имеют смысла вне этого контекста.
- Generic ограничения делают невозможным использование environment для передачи значений counters между разными Tab Groups.
Стандартные компоненты и производительность
Применение стандартных библиотек
- Рассмотрение стандартного компонента image с паттерном builder для изменения размера изображения. Этот подход делает поведение специфичным только для данного компонента.
Сравнение Swift и SwiftUI
Сложности в сравнении
- Сравнение Swift с другими инструментами затруднено из-за отсутствия необходимых инструментов для анализа.
- Тайминги и анимации в SwiftUI отличаются, что делает сравнение сложным.
Инструменты для улучшения производительности
- Основной инструмент для дебаггинга - метод
self print changes, который не имеет хорошей документации.
- Этот метод позволяет отслеживать изменения и причины вызова
bodyвьюхи, что полезно для выявления нежелательных изменений.
Профилировщик Xcode Instruments
- Профилировщик предоставляет шаблоны под SwiftUI, позволяя отслеживать количество вызовов
body.
- Он также отслеживает изменения динамических свойств, таких как
state bindingиenvironment.
Пример использования методов
Описание кейса
- Пример основан на реальном проекте, где пользователю нужно отправить фидбэк через текстовое поле с анимацией.
- Включает выбор эмоций пользователем и кнопку для упрощения дебага.
Анализ производительности
- Используя метод
self print changes, видно, что каждый этап вызывает изменения вbody, что приводит к повторному пересчету лейаута.
- В тестовом сценарии зафиксировано 299 вызовов
bodyпо всем вьюхам.
Оптимизация структуры кода
Изменение архитектуры
- Логика была разделена: слайдер с эмоджиками вынесен в отдельную вьюху, а текстовое поле - в другую.
- Новый контент-вью стал проще и меньше по размеру.
Результаты оптимизации
- Метод
self print changesпоказал только один вызов при первом рендеринге нового контент-вью.
- Инструменты показали снижение количества вызовов до 184, что составляет уменьшение на 40% по сравнению с предыдущим вариантом.
Обсуждение оптимизации в Swift UI
Передача данных и бандинг
- В поле текст передаётся только через бандинг, а сама вьюха не использует его напрямую. Это также касается рейтинга, который меняется, но не отслеживается в Content View.
Оптимизация Swift UI
- Swift UI способен оптимизировать обновления интерфейса, понимая, что структура родительского элемента не изменится. Он пересчитывает только вложенные элементы.
Тестирование интерфейсов
- Обсуждается важность тестирования пользовательского интерфейса (UI-тесты), включая использование стандартных Apple UI-тестов для проверки элементов на экране.
Проблемы с фреймворком тестирования
- Возникают проблемы с иерархией элементов в UI-тестах, которая отличается от той, что используется в UIKit или Swift UI. Это приводит к нестабильным тестам.
Фреймворк Mixbox
- Mixbox — это разработанный фреймворк для более стабильного тестирования. Он создает иерархию элементов на стороне приложения и позволяет использовать более очевидную логику для тестирования.
Интеграция с Swift UI
- Несмотря на отсутствие доступа к кофреймам в Swift UI, Mixbox позволяет проводить тестирование через систему UI hosting view и accessibility.
Снапшот-тесты
- Использование снапшот-тестов для проверки компонентов дизайн-системы помогает избежать неожиданных изменений во внешнем виде элементов.
Проблемы интеграции со SnapShot Testing
- При интеграции снапшот-тестирования могут возникнуть сложности из-за изоляции методов класса Image Render. Это требует изменения сигнатур функций.
Ограничения отображения вьюх
- Вьюхи, использующие UI hosting view для отображения, могут неправильно рендериться или отображаться как жёлтые прямоугольники из-за особенностей реализации Apple.
Заключение и ресурсы
- Участникам предоставляется QR-код со ссылкой на проект с примерами использования обсуждаемых технологий. Все примеры будут доступны для изучения после презентации.
Вопросы и ответы
- После завершения доклада начинается сессия вопросов и ответов; участники могут задавать вопросы по теме обсуждения.
Влияние вложенности вью на производительность
Вопрос о производительности
- Косарин задаёт вопрос о влиянии большой вложенности вью на производительность и возможных способах оптимизации.
- По мнению спикера, большая вложенность не должна значительно влиять на производительность, так как Apple активно схлопывает иерархию.
Иерархия вью
- При использовании UI debugger можно увидеть, что количество отрисовываемых прямоугольников меньше, чем количество вьюшек, которые наслаиваются друг на друга.
- SwiftUI может использовать более простые примитивы для отрисовки элементов, скрывая сложность иерархии от разработчика.
Переход к новой парадигме верстки
- Павел Иванов поднимает вопрос о смене парадигмы верстки для разработчиков, привыкших к UIKit.
- Разработчики могут испытывать трудности с новыми концепциями, такими как падинги и спейсеры в SwiftUI.
Обсуждение структуры лейаута
- Спикер согласен с тем, что многие аспекты лейаута не были глубоко раскрыты во время сессии.
- Упоминается наличие других сессий на конференции, где подробно обсуждаются правила верстки и использование различных компонентов.
Заключение
- В завершение обсуждения упоминается предстоящий доклад Ромы Мерзаяна о внедрении SwiftUI в продакшен.