Создание пользовательского (индивидуального) макета в SwiftUI. Основы.

24 ноября 2022

16 ноября 2022 года

В настоящее время SwiftUI предоставляет протокол Layout, позволяющий нам создавать суперпользовательские (сверхиндивидуальные мне кажется здесь больше подходит) макеты, копаясь в системе компоновки без использования GeometryReader. Протокол Layout дает нам невероятную силу создания и повторного использования любого макета, который вы можете себе представить. На этой неделе мы узнаем, как использовать новый протокол Layout для создания макета потока в SwiftUI.

Любой макет, который вы хотите создать, должен соответствовать новому Layout протоколу. Для реализации у него есть две необходимые функции.

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public protocol Layout : Animatable {
    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout Self.Cache
    ) -> CGSize
    
    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout Self.Cache
    ) 
}

 

  1. Функция sizeThatFits должна вычислить и вернуть окончательный размер вашего макета.
  2. Функция placeSubviews должна размещать сабвью в соответствии с вашими правилами компоновки.

Сегодня мы начнем изучать протокол Layout, реализуя flow layout (схема или макет потока) в SwiftUI. Flow layout обычно ведет себя как HStack, но он отбрасывает линию, как только вью заполняют доступное горизонтальное пространство.

Давайте начнем с расчета окончательного размера нашего flow layout. Он должен произвести итерацию по всем сабвью и суммировать ширину данных  вью до тех пор, пока у нас не появится доступное пространство по горизонтали. Как только мы достигнем конца этого пространства, мы должны обозначить внизу линию расположения и продолжить размещать новые вью от этой линии.

struct FlowLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        
        var totalHeight: CGFloat = 0
        var totalWidth: CGFloat = 0
        
        var lineWidth: CGFloat = 0
        var lineHeight: CGFloat = 0
        
          for size in sizes {
            if lineWidth + size.width > proposal.width ?? 0 {
                totalHeight += lineHeight
                lineWidth = size.width
                lineHeight = size.height
            } else {
                lineWidth += size.width
                lineHeight = max(lineHeight, size.height)
            }

            totalWidth = max(totalWidth, lineWidth)
        }

        totalHeight += lineHeight
        
        return .init(width: totalWidth, height: totalHeight)
    }
}


В примере выше у нас есть реализация функции sizeThatFits. Эта функция предоставляет несколько интересных параметров, которые мы будем использовать для расчета окончательного размера нашего макета. Первый - это ProposedViewSize, предоставляющий нам размер, который предлагает родитель. Второй - это экземпляр класса с типом Subviews, который является прокси-экземпляром для коллекции сабвью, позволяющий нам размещать и измерять размер данных вью.

Мы используем экземпляр прокси (*с полномочиями) Subviews для итерации всех дочерних элементов и расчета их идеальных размеров с помощью функции sizeThatFits. Он принимает параметр, позволяющий нам получить его минимальные, максимальные и идеальные размеры. В приведенном выше примере мы используем параметр unspecified (неуказанный), который означает идеальный размер. Но вы также можете использовать экземпляры zero и infinity, чтобы получить его минимальный и максимальный размер соответственно.

После расчета идеальных размеров для всех вью, мы проходим по ним, чтобы рассчитать общую ширину и высоту окончательного макета. Мы применяем параметр ProposedViewSize, чтобы понять ширину, которую предоставляет нам родительский вью, - так мы узнаем, когда нам нужно бросить линию макета потока.

Наконец, мы создаем и возвращаем окончательный размер макета потока, используя все знания, которые предоставляет нам система компоновки SwiftUI. Теперь мы можем перейти к деталям реализации функции placeSubviews.

struct FlowLayout: Layout {
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        
        var lineX = bounds.minX
        var lineY = bounds.minY
        var lineHeight: CGFloat = 0
        
        for index in subviews.indices {
            if lineX + sizes[index].width > (proposal.width ?? 0) {
                lineY += lineHeight
                lineHeight = 0
                lineX = bounds.minX
            }
            
            subviews[index].place(
                at: .init(
                    x: lineX + sizes[index].width / 2,
                    y: lineY + sizes[index].height / 2
                ),
                anchor: .center,
                proposal: ProposedViewSize(sizes[index])
            )
            
            lineHeight = max(lineHeight, sizes[index].height)
            lineX += sizes[index].width
        }
    }
}


Как вы можете видеть в примере выше, функция placeSubviews имеет тот же набор параметров, что и  sizeThatFits, но также предоставляет нам прямоугольник с границами. Прямоугольник с границами - это место в иерархии вью, которое мы заполним нашими сабвью. Пожалуйста, не стоит полагать, что у него нулевое начало, так как его можно разместить в любом месте экрана; и чтобы идеально разместить свои вью, вам следует использовать свойства minX, minY, maxX, maxY, midX, midY,.

Тип Subviews позволяет нам получить доступ к прокси вью по индексу и использовать его функцию place для размещения вью в определенном положении. Это также позволяет нам перемещать точку привязки вью в соответствии с позицией, которую мы проходим.

struct ContentView: View {
    var body: some View {
        FlowLayout {
            ForEach(0..<5) { _ in
                Group {
                    Text("Hello")
                        .font(.largeTitle)
                    Text("World")
                        .font(.title)
                    Text("!!!")
                        .font(.title3)
                }
                .border(Color.red)
            }
        }
    }
}

 

Сегодня мы изучили основы протокола Layout и создали элементарную версию flow layout. Мы продолжим копаться в протоколе Layout в следующих постах, чтобы создавать более гибкие конфигурации. Не стесняйтесь следить за мной в Twitter и задавать свои вопросы, связанные с этим постом. Спасибо за прочтение, увидимся на следующей неделе!

 

Оригинал статьи

Содержание