Как стать автором
Обновить
1301.79
OTUS
Цифровые навыки от ведущих экспертов

Создание кастомного макета в SwiftUI. Интервалы

Время на прочтение5 мин
Количество просмотров1K
Автор оригинала: Majid

С помощью нескольких макетов мы можем компоновать представления различными способами. Одним из важнейших параметров является интервал (spacing) между дочерними элементами конкретного макета. На этой неделе мы узнаем, как создать кастомный макет, позволяющий задавать определенное расстояние между представлениями, и как соблюдать платформенно-ориентированные предопределенные правила интервалов в 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 с последующим представлением в качестве параметра для вычисления предпочтительного интервала между двумя представлениями по горизонтальной оси. При необходимости мы также можем определить размер вертикального интервала между представлениями, используя ту же функцию с параметром vertical для вертикальной оси.

Чтобы узнать больше об имплементации кэша макета, посмотрите мою статью "Создание пользовательского макета в 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 и задавать вопросы, связанные с данным постом. Спасибо, что прочитали, и до встречи на следующей неделе!

Кстати, компания Apple недавно анонсировала фреймворк Swift Charts, который мы можем использовать для создания диаграмм в приложениях. Приглашаю всех на бесплатный урок, где рассмотрим возможности API Swift Charts.

Теги:
Хабы:
Всего голосов 5: ↑4 и ↓1+4
Комментарии1

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS