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

Как и многие типы, которые мы создаем в Swift, тип, соответствующий протоколу Layout, может определять свои свойства и инициализировать их с помощью функции init. Наш тип FlowLayout - не исключение. Давайте добавим свойство spacing к типу FlowLayout.

struct FlowLayout: Layout {
    var spacing: CGFloat = 0
    
    struct Cache {
        var sizes: [CGSize] = []
    }
    
    func makeCache(subviews: Subviews) -> Cache {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        return Cache(sizes: sizes)
    }
}

Чтобы узнать больше об основах протокола Layout, взгляните на мою приуроченную статью «Создание пользовательского макета в SwiftUI. Основы».


struct ContentView: View {
    var body: some View {
        FlowLayout(spacing: 8) {
            Text("Hello")
                .font(.largeTitle)
            Text("World")
                .font(.title)
            Text("!!!")
                .font(.title3)
        }
        .border(Color.red)
    }
}

Как видно из примера выше, теперь мы можем разместить экземпляр типа FlowLayout с определенным интервалом между вью. Но сначала мы должны настроить функцию sizeThatFits, чтобы она учитывала расстояние между вью при расчете окончательного размера макета.

struct FlowLayout: Layout {
//  ....
    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout Cache
    ) -> CGSize {
        var totalHeight = 0.0
        var totalWidth = 0.0
        
        var lineWidth = 0.0
        var lineHeight = 0.0
        
        for index in subviews.indices {
            if lineWidth + cache.sizes[index].width > proposal.width ?? 0 {
                totalHeight += lineHeight
                lineWidth = cache.sizes[index].width
                lineHeight = cache.sizes[index].height
            } else {
                lineWidth += cache.sizes[index].width + spacing
                lineHeight = max(lineHeight, cache.sizes[index].height)
            }
            
            totalWidth = max(totalWidth, lineWidth)
        }
        
        totalHeight += lineHeight
        
        return .init(width: totalWidth, height: totalHeight)
    }
}

Во-вторых, мы должны добавить интервалы между вью при размещении их в функции placeSubviews.

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

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

Предпочтительный интервал

Как видите, большинство макетов в SwiftUI позволяют нам привести интервал к nil, когда макет использует предпочтительный интервал вместо нулевого. У SwiftUI есть настройки интервалов между вью. Например, предпочтительный интервал между вью «Image» и «Text», но это значение может отличаться от «Text»-а к «Text»-у. И эти настройки интервала могут быть разными для iOS, macOS, watchOS и tvOS.

К счастью, SwiftUI предоставляет API для расчета расстояния между вью с учетом интервальных настроек, ориентированных на платформу. Этот API является частью протокола Layout и находится в типе прокси Subview.

struct FlowLayout: Layout {
    var spacing: CGFloat? = nil
    
    struct Cache {
        var sizes: [CGSize] = []
        var spacing: [CGFloat] = []
    }
    
    func makeCache(subviews: Subviews) -> Cache {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        let spacing: [CGFloat] = subviews.indices.map { index in
            guard index != subviews.count - 1 else {
                return 0
            }
            
            return subviews[index].spacing.distance(
                to: subviews[index+1].spacing,
                along: .horizontal
            )
        }
        
        return Cache(sizes: sizes, spacing: spacing)
    }
}

Давайте начнем с добавления свойства spacing в наш кеш. Это идеальный кандидат для размещения в кеше, т.к. мы хотим вычислять его сразу, как только меняется список сабвью.

Далее мы перебираем наши сабвью и используем свойство spacing в типе Subview, чтобы вызвать функцию distance со следующим вью в качестве параметра для вычисления предпочтительного интервала между двумя вью по горизонтальной оси. Мы также можем измерить вертикальный интервал между вью, если это необходимо, используя ту же функцию с параметром вертикальной оси.


Чтобы узнать больше о реализации кеша макета, взгляните на мою приуроченную статью «Создание пользовательского макета в SwiftUI. Кэширование».


Давайте настроим функцию sizeThatFits для учета интервала между вью при расчете окончательного размера макета.

struct FlowLayout: Layout {
    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout Cache
    ) -> CGSize {
        var totalHeight = 0.0
        var totalWidth = 0.0
        
        var lineWidth = 0.0
        var lineHeight = 0.0
        
        for index in subviews.indices {
            if lineWidth + cache.sizes[index].width > proposal.width ?? 0 {
                totalHeight += lineHeight
                lineWidth = cache.sizes[index].width
                lineHeight = cache.sizes[index].height
            } else {
                lineWidth += cache.sizes[index].width + (spacing ?? cache.spacing[index])
                lineHeight = max(lineHeight, cache.sizes[index].height)
            }
            
            totalWidth = max(totalWidth, lineWidth)
        }
        
        totalHeight += lineHeight
        
        return .init(width: totalWidth, height: totalHeight)
    }

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

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

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

struct ContentView: View {
    var body: some View {
        FlowLayout {
            Text("Hello")
                .font(.largeTitle)
            Text("World")
                .font(.title)
            Text("!!!")
                .font(.title3)
        }
        .border(Color.red)
    }
}

Надеюсь, вам понравится пост. Не стесняйтесь подписываться на меня в Twitter и задавать вопросы, связанные с этим постом. Спасибо за прочтение, и увидимся на следующей неделе!

Комментарии (0)