Как создать динамический макет коллекции в Swiftui
Введение в динамические представления коллекций в SwiftUI
Современные приложения iOS требуют плавных, адаптивных интерфейсов, которые изящно реагируют на различные размеры данных, ориентации устройств и размеры экрана. В UIKit представления о сборке обеспечивают мощный, но многословный механизм для создания макетов, подобных сетке. SwiftUI значительно упрощает это с помощью декларативного API, особенно через LazyVGrid и LazyHGrid . Эти компоненты позволяют разработчикам определять динамические, значительные сетки с минимальным кодом, что облегчает создание готовых к производству пользовательских интерфейсов, которые работают в iPhone, iPad и даже Mac Catalyst.
динамическая компоновка просмотра коллекции адаптирует свой счетчик столбцов, высоту строк, расстояние или размерное поведение на основе доступной ширины контейнера, количества элементов или самого контента. Эта статья проведет вас через создание таких макетов в SwiftUI, охватывая все, от базовой настройки сетки до продвинутого адаптивного поведения, настройки производительности и интеграции с анимацией. В конце у вас будет прочная основа для создания гибких, привлекательных и эффективных просмотров коллекций в ваших собственных приложениях.
Основные компоненты: LazyVGrid и LazyHGrid
SwiftUI предоставляет два основных контейнера сетки: LazyVGrid (вертикальная сетка прокрутки) и LazyHGrid (горизонтальная сетка прокрутки). Оба лениво загружают своих детей, то есть представления создаются только по мере их появления на экране — важнейшая функция для обработки больших наборов данных без раздувания памяти. Эти контейнеры аналогичны в UIKit, за вычетом шаблона источника данных и протоколов делегирования.
GridItem Конфигурация
Для определения структуры сетки вы создаете массив экземпляров GridItem. Каждый представляет собой один столбец (в ) или один ряд (в ). Вы управляете тремя ключевыми свойствами:
- Размер — один из трёх режимов калибровки: , или .
- Размещение — Горизонтальное расстояние между объектами в одном ряду (для вертикальных сеток) или вертикальное расстояние (для горизонтальных сеток).
- Выравнивание — контролирует, как элементы выравниваются в ячейке сетки. Редко меняется от по умолчанию.
Выбор режима размеров определяет общую гибкость компоновки:
- Фиксированная — явная ширина (или высота) для каждого столбца/ряда. Полезно, когда вам нужен точный размер, например, трехколонный макет фиксированных 100-точечных ячеек.
- Гибкий — Каждый элемент сетки может растягиваться, чтобы заполнить доступное пространство, с дополнительными минимальными и максимальными границами.
- Адаптивная — Сетка автоматически определяет, сколько элементов помещается в строку столбца, исходя из заданного минимального размера.Это самый простой способ сделать динамическую компоновку, которая реагирует на ширину экрана.
Например, чтобы создать сетку, которая автоматически помещается как можно больше столбцов, каждая из которых имеет минимальную ширину 100 точек, вы бы написали:
let columns = [
GridItem(.adaptive(minimum: 100))
]
Эта линия дает макет, который адаптируется от одной колонки на узком iPhone к нескольким колонкам на iPad в ландшафте - именно то поведение, которое мы хотим для динамических макетов.
Создание базовой динамической сети
Давайте начнем с простой, полностью динамической сетки, используя размер . Ниже представлен полный вид, который отображает множество строк в отзывчивой сетке:
struct AdaptiveGridView: View {
let items = Array(1...20).map { "Item \($0)" }
var body: some View {
ScrollView {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 80), spacing: 12)],
spacing: 12
) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(minWidth: 80, maxWidth: .infinity, minHeight: 80)
.background(Color.blue.opacity(0.2))
.cornerRadius(8)
}
}
.padding()
}
}
}
Здесь каждый элемент вынужден иметь не менее 80 точек в ширину и высоту. Затем элемент сетки автоматически вычисляет, сколько элементов может поместиться в строку, учитывая доступную ширину, соблюдая минимальную ширину. Результатом является макет, который плавно перетекает при вращении устройства или изменении размера окна (на iPad или Mac).
Этот подход хорошо работает для простого контента, но для большего контроля вам часто нужно комбинировать с элементами или использовать для вычисления пользовательских подсчетов столбцов.
Использование GeometryReader для точного динамического подсчета колонок
Хотя FLT:14 обрабатывает множество случаев, иногда вы хотите установить точное количество столбцов на основе ширины контейнера - например, всегда 2 столбца на iPhone в портрете, 3 в ландшафте и 4 на iPad. Вид GeometryReader обеспечивает текущий размер его родителя, позволяя вам динамически вычислять оптимальное количество столбцов.
Пример: адаптивный счет колонки
В следующем представлении показана имеющаяся ширина и вычисляется количество столбцов на основе ширины целевого элемента:
struct DynamicColumnGridView: View {
let items = Array(1...30).map { "Item \($0)" }
let targetItemWidth: CGFloat = 120
var columns: [GridItem] {
let count = max(Int(geometryProxy.size.width / targetItemWidth), 1)
return Array(repeating: GridItem(.flexible(), spacing: 12), count: count)
}
var body: some View {
GeometryReader { geometry in
ScrollView {
let width = geometry.size.width
let count = max(Int(width / targetItemWidth), 1)
let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: count)
LazyVGrid(columns: columns, spacing: 12) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 80)
.background(Color.green.opacity(0.3))
.cornerRadius(8)
}
}
.padding()
}
}
}
}
Важно: На практике вы должны хранить значение внутри переменной или использовать его в одном и том же закрытии. В приведенном выше примере для ясности используется вспомогательная переменная внутри закрытия. Всегда убедитесь, что вы не оберните все тело в излишне — он может нарушить макет, если используется в качестве верхнего вида.
Этот метод дает вам полный контроль над количеством колонок, сохраняя при этом элементы гибкими, чтобы они равномерно заполняли ширину. Вы также можете добавить минимальные и максимальные ограничения к гибким элементам, чтобы избежать чрезвычайно растянутых или раздавленных ячеек.
Динамично адаптируя высоту
Динамические макеты не только о столбцах - высота строк также может варьироваться в зависимости от контента. SwiftUI не обеспечивает прямую «автоматическую сетку размера», такую как UIKit с предполагаемыми размерами. Однако вы можете достичь аналогичных результатов, используя внутренний размер контента и или путем встраивания высот, управляемых данными.
Например, вы можете сделать каждую ячейку сетки самостоятельной по размеру, опустив явные высоты и позволяя внутреннему контенту определять размер. SwiftUI автоматически вычислит высоту ячейки на основе самого высокого элемента в каждой строке (для вертикальных сеток). Но если вам нужен действительно переменный рост строки (например, макет кладки), вам нужен пользовательский подход - часто используя с пользовательскими руководствами по выравниванию или обертывая сетку в , но с разными высотами в столбце. Это более продвинуто и выходит за рамки этой статьи, но ключевой вывод заключается в том, что предполагает, что все ячейки в строке имеют одинаковую высоту (определяется самой высокой ячейкой). Если ваш дизайн требует переменных высот, рассмотрите возможность использования с пользовательским макетом сетки.
Обработка больших наборов данных с ленивой загрузкой
Одним из главных преимуществ и является их ленивое поведение при загрузке. Они создают просмотры только для элементов, которые в настоящее время видны (плюс небольшой буфер). Это делает их эффективными для наборов данных с сотнями или даже тысячами элементов. Однако есть несколько лучших практик для поддержания плавной производительности:
- Используйте стабильные идентификаторы — Всегда предоставляйте уникальный параметр . Для структурных моделей данных, которые соответствуют , SwiftUI может эффективно диффундировать.
- Избегайте дорогостоящих вычислений внутри тела — Держите просмотр ячеек простым. Если вам нужны сложные расчёты компоновки, предварительно вычислите их за пределами сетки.
- Перенос — Если ваш вид соты соответствует или использует модификатор , SwiftUI может пропустить изменения рендеринга, когда данные соты не изменились.
- Использовать внутри — — Если вы вложите внутри другого ленивого контейнера, вы можете разорвать ленивую погрузку.Как правило, сама сетка должна быть прямым ребенком (или быть завернута внутри одного).
Сочетание динамических макетов с анимациями
Анимации в SwiftUI могут сделать изменения макета естественными и отполированными. Например, при изменении количества столбцов (из-за вращения или изменения размера окна) можно анимировать элементы сетки до их новых позиций. Самый простой способ - обернуть сетку в модификатор или использовать модификатор со значением, которое вызывает изменение.
Анимационные изменения колонны
Вот расширение предыдущего примера динамической колонки с анимированным переходом:
struct AnimatedDynamicGridView: View {
let items = Array(1...20).map { "Item \($0)" }
@State private var targetItemWidth: CGFloat = 120
var body: some View {
GeometryReader { geometry in
ScrollView {
let width = geometry.size.width
let count = max(Int(width / targetItemWidth), 1)
let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: count)
LazyVGrid(columns: columns, spacing: 12) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 80)
.background(Color.orange.opacity(0.4))
.cornerRadius(8)
.transition(.scale.combined(with: .opacity))
}
}
.padding()
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: count)
}
}
}
}
Привязав анимацию к значению , SwiftUI будет анимировать вставку, удаление и перестановку элементов при изменении количества столбцов. Вы также можете использовать для более продвинутых переходов между макетами.
Примеры: Динамические схемы реального мира
Давайте рассмотрим два общих сценария, которые выигрывают от динамических макетов просмотра коллекции.
Фотогалерея Grid
Типичная фотогалерея использует квадратную сетку ячеек, которая адаптируется к ширине экрана. Вы хотите, чтобы ячейки были как можно больше при сохранении фиксированного количества столбцов (например, 2 на iPhone, 3 на iPhone Plus, 4 на iPad). Использование с условным подсчетом столбцов на основе классов размеров просто:
struct PhotoGalleryGrid: View {
let images: [String]
@Environment(\.horizontalSizeClass) var sizeClass
var columns: [GridItem] {
let count = sizeClass == .compact ? 2 : 4
return Array(repeating: GridItem(.flexible(), spacing: 2), count: count)
}
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach(images, id: \.self) { imageName in
Image(imageName)
.resizable()
.aspectRatio(1, contentMode: .fill)
.clipped()
}
}
.padding(.horizontal, 2)
}
}
}
Динамическая панель с различными размерами ячеек
Для панели приборов у вас могут быть ячейки различной ширины - например, большой граф, занимающий два столбца, меньшая статистика, занимающая по одному столбцу каждый. Вы можете достичь этого, объединив элементы с явными множителями с использованием пользовательского массива .
let columns: [GridItem] = [
GridItem(.flexible(minimum: 80), spacing: 12), // small stat cell
GridItem(.flexible(minimum: 160), spacing: 12), // wide cell
GridItem(.flexible(minimum: 80), spacing: 12) // small stat cell
]
Затем в сетке вы можете отображать различные типы данных на разные пролеты столбцов, обертывая элементы в контейнер, который расширяется, чтобы заполнить ширину сетки. Это требует тщательной координации, потому что изначально не поддерживает многостолбцовые пролеты. Общим обходным путем является использование модификатора или ручной индекс сетки, разделяя ваши данные на разделы, каждый из которых заполняет полный ряд.
Для iOS 16 и более поздних версий вы можете использовать модификатор на просмотре внутри сетки, чтобы указать, сколько столбцов должен охватывать просмотр:
LazyVGrid(columns: columns, spacing: 12) {
ForEach(dashboardItems, id: \.id) { item in
DashboardCell(item: item)
.gridCellColumns(item.columnSpan)
}
}
Это самый чистый способ создать настоящую динамическую панель приборов со смешанными размерами ячеек.
Соображения производительности для больших наборов данных
При работе с тысячами предметов даже ленивая загрузка может привести к задержкам, если каждая ячейка сложна. Вот передовые методы, чтобы ваши динамические сетки были гладкими:
- Предварительные изображения и данные — Используйте или для загрузки данных асинхронно, а не в теле клетки.
- Использовать для чрезвычайно больших списков — SwiftUI более оптимизирован для тысяч строк с динамическим размером.Однако для сетки макет с пользовательским макетом возможен, но его труднее достичь; рассмотрите возможность использования через , если производительность становится настоящим узким местом.
- Ограничить расчёт компоновки — Избегайте размещения динамической логики компоновки внутри тела клетки.
- Использование на всей сетке — Если полностью изменить структуру сетки (например, переключаясь с 2 колонок на 3), назначение новой на сетку заставило ее полностью перезагрузиться, что может быть быстрее, чем попытка анимировать сложное изменение.
Внешние ресурсы для углубления ваших знаний
Для дальнейшего изучения динамических представлений о коллекции в SwiftUI обратитесь к этим официальным и общественным ресурсам:
- Документация LazyVGrid от Apple — официальная ссылка на все параметры и поведение.Документация разработчика Apple: LazyVGrid
- Руководство по быстрому запуску для GridItem — охватывает все три стратегии размера с интерактивными примерами.Документация разработчика Apple: GridItem
- SwiftUI by example — Grid Layouts — Практический урок от Hacking with Swift, который охватывает адаптивные и гибкие сетки.Hacking with Swift: Grid Layouts
- Лаборатория SwiftUI: Ленивые сетки — глубокое погружение в производительность и настройки. Лаборатория SwiftUI: Ленивые сетки
Краткое изложение лучших практик
Создание динамической компоновки просмотра коллекции в SwiftUI требует балансировки гибкости с производительностью. Вот быстрый контрольный список для готовых к производству сеток:
- Начните с для простейшего динамического подсчета столбцов; используйте только тогда, когда вам нужен точный контроль.
- Всегда предоставляйте уникальные значения или соответствуйте .
- Используйте со значением, которое изменяется (например, количество столбцов) для плавных переходов.
- Для смешанных размеров ячеек предпочтите на iOS 16+ или создайте пользовательский макет строки за строкой.
- Профилируйте свою сетку с помощью инструментов, чтобы проверить, что ленивая загрузка работает, и ячейки не отображают содержимое за пределами экрана.
- Тестирование на нескольких размерах и ориентации устройства - динамические макеты, которые отлично смотрятся на одном экране, могут сломаться на другом.
Освоение динамических просмотров коллекций позволяет создавать приложения, которые чувствуют себя нативными и отзывчивыми на каждом устройстве Apple. С помощью декларативных инструментов SwiftUI вы можете достичь того, что когда-то требовало десятков строк кода UIKit, всего в нескольких выразительных многоразовых компонентах.