Создание потоковой компоновки
Динамически переносите элементы на следующие строки.
«Создание потоковой компоновки» — бесплатный урок Swift Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.
Что такое компоновка с переносом?
Компоновка с переносом размещает элементы слева направо, перенося следующий элемент на новую строку, если он не помещается в доступную ширину, как бирки или перенос слов.
План
Мы вычислим границы строк во вспомогательной функции, вернём общую высоту из sizeThatFits и повторно используем те же вычисления в placeSubviews. Общая логика поддерживает согласованность двух методов.
Моделирование размещения
Небольшая структура записывает, где должно находиться каждое дочернее представление. Предварительное вычисление всех размещений делает оба метода протокола простыми.
struct Placement {
let index: Int
let point: CGPoint
let size: CGSize
}Алгоритм переноса
Пройдите по дочерним представлениям, отслеживая текущую координату x. Если дочернее представление превысит максимальную ширину, сбросьте x и переместитесь вниз на высоту строки плюс расстояние.
func layout(_ subviews: Subviews, maxWidth: CGFloat) -> (placements: [Placement], height: CGFloat) {
var placements: [Placement] = []
var x: CGFloat = 0, y: CGFloat = 0, rowHeight: CGFloat = 0
let spacing: CGFloat = 8
for (i, s) in subviews.enumerated() {
let size = s.sizeThatFits(.unspecified)
if x + size.width > maxWidth, x > 0 {
x = 0; y += rowHeight + spacing; rowHeight = 0
}
placements.append(Placement(index: i, point: CGPoint(x: x, y: y), size: size))
x += size.width + spacing
rowHeight = max(rowHeight, size.height)
}
return (placements, y + rowHeight)
}Реализация sizeThatFits
sizeThatFits выполняет алгоритм для предложенной ширины и возвращает ширину предложения и вычисленную общую высоту.
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxWidth = proposal.width ?? .infinity
let result = layout(subviews, maxWidth: maxWidth)
return CGSize(width: maxWidth == .infinity ? result.placements.map { $0.point.x + $0.size.width }.max() ?? 0 : maxWidth,
height: result.height)
}Реализация placeSubviews
placeSubviews выполняет тот же алгоритм, затем смещает каждое размещение на bounds.origin и вызывает place.
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let result = layout(subviews, maxWidth: bounds.width)
for p in result.placements {
let pt = CGPoint(x: bounds.minX + p.point.x, y: bounds.minY + p.point.y)
subviews[p.index].place(at: pt, anchor: .topLeading,
proposal: ProposedViewSize(p.size))
}
}Полный тип FlowLayout
Объединение всех частей даёт полный повторно используемый макет с переносом. Общая вспомогательная функция layout гарантирует согласованность размера и размещения.
struct FlowLayout: Layout {
var spacing: CGFloat = 8
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let r = layout(subviews, maxWidth: proposal.width ?? .infinity)
return CGSize(width: proposal.width ?? 0, height: r.height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let r = layout(subviews, maxWidth: bounds.width)
for p in r.placements {
subviews[p.index].place(at: CGPoint(x: bounds.minX + p.point.x, y: bounds.minY + p.point.y),
anchor: .topLeading, proposal: ProposedViewSize(p.size))
}
}
}Использование макета с переносом
Используйте его как любой контейнер. При уменьшении ширины бирки автоматически переносятся.
var body: some View {
FlowLayout {
ForEach(tags, id: \.self) { tag in
Text(tag)
.padding(8)
.background(.blue.opacity(0.2))
.clipShape(Capsule())
}
}
}Обработка первого элемента в строке
Условие x > 0 предотвращает перенос перед первым элементом в строке. Без него элемент, ширина которого превышает ширину контейнера, мог бы зациклиться или без необходимости переместиться в пустую строку.
if x + size.width > maxWidth, x > 0 {
// wrap only if the row already has content
}Варианты задания расстояния
Здесь расстояние хранится как свойство со значением по умолчанию. В рабочем приложении считывайте предпочтение spacing каждого дочернего представления, чтобы учитывать системные метрики, а не использовать жёстко заданную константу.
var spacing: CGFloat = 8 // configurable per instanceПроверка крайних случаев
Проверьте работу с нулевым количеством элементов, одним огромным элементом и множеством маленьких элементов. Правильный макет с переносом сообщает нулевую высоту для пустого содержимого и никогда не обрезает единственное слишком большое дочернее представление.
Быстрая проверка: макет с переносом
Проверьте понимание алгоритма переноса.
Итоги: создание макета с переносом
Макет с переносом размещает элементы, отслеживая текущую координату x и переходя на новую строку, когда следующий элемент не помещается. Общая вспомогательная функция вычисляет все размещения и общую высоту, поэтому sizeThatFits и placeSubviews остаются согласованными.
Смещайте размещения на bounds.origin, обрабатывайте первый элемент каждой строки отдельно и проверяйте случаи с пустым, слишком большим и большим количеством элементов. В результате получается повторно используемый контейнер с переносом.
Изучай Swift с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 122
- Уроки
- 409
Часто задаваемые вопросы
Урок «Создание потоковой компоновки» бесплатный?
Да — полный текст урока «Создание потоковой компоновки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.
Чему я научусь в уроке «Создание потоковой компоновки»?
Динамически переносите элементы на следующие строки. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Swift Academy?
Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Создание потоковой компоновки»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Swift Academy?
Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Основы протокола Layout
- Измерение подвида
- Создание потоковой компоновки
- Кэш компоновки и производительность