Как создать динамический макет коллекции в Swiftui

Введение в динамические представления коллекций в SwiftUI

Современные приложения iOS требуют плавных, адаптивных интерфейсов, которые изящно реагируют на различные размеры данных, ориентации устройств и размеры экрана. В UIKit представления о сборке обеспечивают мощный, но многословный механизм для создания макетов, подобных сетке. SwiftUI значительно упрощает это с помощью декларативного API, особенно через LazyVGrid и LazyHGrid . Эти компоненты позволяют разработчикам определять динамические, значительные сетки с минимальным кодом, что облегчает создание готовых к производству пользовательских интерфейсов, которые работают в iPhone, iPad и даже Mac Catalyst.

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

Основные компоненты: LazyVGrid и LazyHGrid

SwiftUI предоставляет два основных контейнера сетки: LazyVGrid (вертикальная сетка прокрутки) и LazyHGrid (горизонтальная сетка прокрутки). Оба лениво загружают своих детей, то есть представления создаются только по мере их появления на экране — важнейшая функция для обработки больших наборов данных без раздувания памяти. Эти контейнеры аналогичны в UIKit, за вычетом шаблона источника данных и протоколов делегирования.

GridItem Конфигурация

Для определения структуры сетки вы создаете массив экземпляров GridItem. Каждый представляет собой один столбец (в ) или один ряд (в ). Вы управляете тремя ключевыми свойствами:

Выбор режима размеров определяет общую гибкость компоновки:

Например, чтобы создать сетку, которая автоматически помещается как можно больше столбцов, каждая из которых имеет минимальную ширину 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 могут сделать изменения макета естественными и отполированными. Например, при изменении количества столбцов (из-за вращения или изменения размера окна) можно анимировать элементы сетки до их новых позиций. Самый простой способ - обернуть сетку в модификатор или использовать модификатор со значением, которое вызывает изменение.

Анимационные изменения колонны

Вот расширение предыдущего примера динамической колонки с анимированным переходом:

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 обратитесь к этим официальным и общественным ресурсам:

  1. Документация LazyVGrid от Apple — официальная ссылка на все параметры и поведение.Документация разработчика Apple: LazyVGrid
  2. Руководство по быстрому запуску для GridItem — охватывает все три стратегии размера с интерактивными примерами.Документация разработчика Apple: GridItem
  3. SwiftUI by example — Grid Layouts — Практический урок от Hacking with Swift, который охватывает адаптивные и гибкие сетки.Hacking with Swift: Grid Layouts
  4. Лаборатория SwiftUI: Ленивые сетки — глубокое погружение в производительность и настройки. Лаборатория SwiftUI: Ленивые сетки

Краткое изложение лучших практик

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

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