Swift Academy · Урок

Создание потоковой компоновки

Динамически переносите элементы на следующие строки.

Урок 3 из 413 шагов

«Создание потоковой компоновки» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Основы протокола Layout
  2. Измерение подвида
  3. Создание потоковой компоновки
  4. Кэш компоновки и производительность
← Назад к Swift Academy