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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

С помощью нескольких макетов мы можем компоновать представления различными способами. Одним из важнейших параметров является интервал (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.

  • Зарегистрироваться на бесплатный урок

Источник: https://habr.com/ru/company/otus/blog/711912/


Интересные статьи

Интересные статьи

Привет, Хабр! Решила с вами поделиться одной простой работой, которая привела к неплохим результатам. Расскажу о всем подробно и очень просто:) Интересно тем, кто еще не решал задачи NLP до этого...
Смелые архитектурные решения, которые легли в основу самого продаваемого компьютера всех времен и народов.В январе 1981 года несколько инженеров компании MOS Technology начали разрабатывать графически...
Рано или поздно, каждый пэхапешник, пишущий на битриксе, начинает задумываться о том, как бы его улучшить, чтобы и всякие стандарты можно было соблюдать, и современные инструменты разработки использов...
Перевод подготовлен специально для будущих студентов курса "Разработчик на Spring Framework".Эта статья о динамическом создании бинов за пять лет стала самой популярной в...
В «1С-Битрикс» считают: современный интернет-магазин должен быть визуально привлекательным, адаптированным для просмотра с мобильных устройств и максимально персонализированным с помощью технологии Бо...