0Pricing
Swift Academy · Урок

Основы протокола Layout

Реализуйте sizeThatFits и placeSubviews.

«Основы протокола Layout» — бесплатный урок Swift Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.

Зачем нужна пользовательская компоновка

SwiftUI предоставляет HStack, VStack и ZStack, но иногда требуется логика размещения, которую они не могут выразить: радиальные меню, потоковые компоновки, мозаичные сетки.

Протокол компоновки позволяет написать такую логику и при этом по-прежнему сочетать её с обычными представлениями SwiftUI.

Протокол компоновки

Layout требует двух методов: sizeThatFits (каков мой размер?) и placeSubviews (куда поместить каждого дочернего элемента?).

Реализуйте это соответствие для типа-значения и используйте его как любой встроенный стек.

struct MyLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize,
                      subviews: Subviews,
                      cache: inout ()) -> CGSize { .zero }
    func placeSubviews(in bounds: CGRect,
                       proposal: ProposedViewSize,
                       subviews: Subviews,
                       cache: inout ()) { }
}

Использование пользовательской компоновки

Значение Layout выступает в роли контейнера. Передавайте дочерние представления в его замыкании в конце вызова точно так же, как в стеке.

var body: some View {
    MyLayout {
        Text("One")
        Text("Two")
        Text("Three")
    }
}

ProposedViewSize

Родительский элемент предлагает размер Вашей компоновке через ProposedViewSize. Его значения width и height являются опциональными: nil означает «размер не задан, выберите размер ideal».

Специальные значения .zero, .infinity и .unspecified позволяют получить минимальный, максимальный и ideal размеры.

func sizeThatFits(proposal: ProposedViewSize,
                  subviews: Subviews,
                  cache: inout ()) -> CGSize {
    let width = proposal.width ?? 0
    return CGSize(width: width, height: 44)
}

Подробно о sizeThatFits

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

func sizeThatFits(proposal: ProposedViewSize,
                  subviews: Subviews,
                  cache: inout ()) -> CGSize {
    // Sum child ideal sizes for a simple horizontal stack
    let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
    let w = sizes.reduce(0) { $0 + $1.width }
    let h = sizes.map(\.height).max() ?? 0
    return CGSize(width: w, height: h)
}

Коллекция дочерних представлений

Subviews — это коллекция прокси-объектов для каждого дочернего элемента. Каждый прокси отвечает на вызов sizeThatFits и предоставляет приоритеты компоновки и пользовательские значения.

Вы никогда не видите настоящие представления — только эти прокси для измерений.

for subview in subviews {
    let size = subview.sizeThatFits(.unspecified)
    print(size)
}

placeSubviews

placeSubviews назначает каждому дочернему элементу позицию внутри переданного Вам прямоугольника bounds.

Вызовите place(at:anchor:proposal:) для каждого прокси дочернего элемента.

func placeSubviews(in bounds: CGRect,
                   proposal: ProposedViewSize,
                   subviews: Subviews,
                   cache: inout ()) {
    var x = bounds.minX
    for subview in subviews {
        let size = subview.sizeThatFits(.unspecified)
        subview.place(at: CGPoint(x: x, y: bounds.midY),
                      anchor: .leading,
                      proposal: .unspecified)
        x += size.width
    }
}

Якоря при размещении

Параметр anchor указывает, какая точка дочернего элемента окажется в заданной Вами позиции. .topLeading размещает элемент по верхнему левому углу; .center центрирует дочерний элемент относительно этой точки.

subview.place(at: CGPoint(x: 0, y: 0),
              anchor: .topLeading,
              proposal: .unspecified)

Границы не всегда начинаются в начале координат

Прямоугольник bounds может начинаться не в точке (0,0). Всегда размещайте элементы относительно bounds.minX и bounds.minY, никогда не используя абсолютный ноль.

let startX = bounds.minX // not 0
let startY = bounds.minY

Минимальная компоновка без промежутков

Объединим всё: вот компоновка, размещающая дочерние представления слева направо без промежутков. Два метода согласованы: сообщаемый размер соответствует размещению.

struct SimpleHStack: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        return CGSize(width: sizes.reduce(0){ $0 + $1.width },
                      height: sizes.map(\.height).max() ?? 0)
    }
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var x = bounds.minX
        for s in subviews {
            let size = s.sizeThatFits(.unspecified)
            s.place(at: CGPoint(x: x, y: bounds.minY), anchor: .topLeading, proposal: .unspecified)
            x += size.width
        }
    }
}

Согласованность — Ваша задача

SwiftUI рассчитывает, что два Ваших метода согласуются. Если placeSubviews использует больше места, чем сообщил sizeThatFits, дочерние элементы могут обрезаться или перекрываться.

Используйте общие геометрические вычисления в обоих методах.

Быстрая проверка: основы компоновки

Проверьте своё понимание протокола компоновки.

Итоги: основы протокола компоновки

Протокол Layout превращает пользовательскую логику размещения в повторно используемый контейнер. sizeThatFits сообщает размер для ProposedViewSize, а placeSubviews размещает каждый прокси дочернего элемента внутри bounds с помощью place(at:anchor:proposal:).

Предложения содержат опциональные размеры, границы могут быть смещены относительно начала координат, а два метода должны оставаться геометрически согласованными. Далее мы подробно измерим дочерние представления.

Часто задаваемые вопросы

Урок «Основы протокола Layout» бесплатный?

Да — полный текст урока «Основы протокола Layout» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.

Чему я научусь в уроке «Основы протокола Layout»?

Реализуйте sizeThatFits и placeSubviews. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Swift Academy?

Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Основы протокола Layout»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Swift Academy?

Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

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

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