Отображение списков с помощью UICollectionViewCompositionalLayout в iOS
В этом году Apple провела «взрывную» WWDC. Все сообщество iOS-разработчиков сфокусировалось на новых фреймворках (SwiftUI, Combine, RealityKit...), пытаясь разобраться, как это работает и что нового принесет. Многие небольшие, но очень полезные для актуальных приложений обновления почему-то остались за кадром. Тем не менее я хочу поделиться моими исследованиями одного из них — UICollectionViewCompositionalLayout.
Я уверен, каждый из начинающих iOS-разработчиков начинал свое обучение с изучения списков. Это тот незаменимый UI-элемент, который присутствует практически в каждом приложении. UICollectionView — это эволюционировавший UITableView, который позволяет размещать элементы списка в разном порядке с помощью UICollectionViewLayout. Тот, кто хоть раз пробовал создать свой кастомный CollectionViewLayout, знает, как это непросто. Мы имеем море методов, с помощью которых нужно задать поведение. Не все их нужно использовать, иногда нужно написать дополнительное кеширование. Все это создает сложности, с которыми редко какой разработчик хочет столкнуться. Вот почему в этом году Apple представила класс, который продолжает направление декларативного программирования, — UICollectionViewCompositionalLayout.
UICollectionViewCompositionalLayout — это Layout, с помощью которого можно задать поведение элементов при отображении их в UICollectionView. После того как будет сформулировано, как отображать эти элементы, Layout сделает всю остальную работу за нас. То есть уже не нужно будет заботиться о всех тех методах, которые нужно имплементировать при расширении класса UICollectionViewLayout.
Давайте рассмотрим простой пример. Код, приведенный ниже, позволяет создать список, в котором высота каждой ячейки одинаковая.
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}

Далее я рассмотрю все классы, которые участвуют в построении этого Layout.
NSCollectionLayoutSize
NSCollectionLayoutSize позволяет задать ширину и высоту элемента в Layout. Имеет две переменные, которые задаются при инициализации:
open var widthDimension: NSCollectionLayoutDimension { get }
open var heightDimension: NSCollectionLayoutDimension { get }
NSCollectionLayoutDimension, в свою очередь, позволяет задать размер в четырех вариантах:
//dimension is computed as a fraction of the width of the containing group
open class func fractionalWidth(_ fractionalWidth: CGFloat) -> Self
//dimension is computed as a fraction of the height of the containing group
open class func fractionalHeight(_ fractionalHeight: CGFloat) -> Self
// dimension with an absolute point value
open class func absolute(_ absoluteDimension: CGFloat) -> Self
// dimension is estimated with a point value. Actual size will be determined when the content is rendered.
open class func estimated(_ estimatedDimension: CGFloat) -> Self
fractionalWidth — размер относительно ширины группы или контейнера, который содержит группу.
fractionalHeight — размер относительно высоты группы или контейнера, который содержит группу.
absolute — абсолютное значение в пойнтах.
estimated — предположительное значение в пойнтах; реальное значение будет установлено в зависимости от контента.
Есть одна интересная особенность: в NSCollectionLayoutSize.widthDimension мы можем задавать fractionalHeight и, наоборот, в NSCollectionLayoutSize.heightDimension — относительную ширину (fractionalWidth). То есть, например, NSCollectionLayoutSize.heightDimension = .fractionalWidth(0.5) — высота элемента равна половине ширины группы.
estimated — это, наверное, самое интересное из всех. Я думаю, любой, кто работал с коллекциями, знает, что текущая реализация не очень хорошо работает со сложными ячейками. Я протестировал новую реализацию и могу сказать, что она работает намного лучше предыдущей. Для того чтобы это сработало, у элемента и у группы должна быть выставлена высота с типом estimated.
Также нельзя задавать contentInsets для конкретного элемента, иначе Layout начинает вести себя непредсказуемо.
Ниже приведен пример Layout для списка, в котором автоматически рассчитываются размеры элементов.
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(50))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: .fixed(8), trailing: nil, bottom: .fixed(8))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}

NSCollectionLayoutItem
NSCollectionLayoutItem отвечает за расположение элемента в группе, его инициализация происходит с помощью NSCollectionLayoutSize. Кроме этого, в нем есть две переменные, позволяющие дополнительно задать отступы для элемента.
open var contentInsets: NSDirectionalEdgeInsets
open var edgeSpacing: NSCollectionLayoutEdgeSpacing?
contentInsets отвечает за отступы элемента, после того как он уже размещен в Layout. Как отмечено выше, не стоит использовать эти отступы с предположительными размерами элемента (.estimated).
edgeSpacing более интересен, потому что это отдельный класс NSCollectionLayoutEdgeSpacing. В этом классе можно задать отступы leading, top, trailing, bottom, но с помощью NSCollectionLayoutSpacing. Эти отступы учитываются при размещении элемента в Layout. Можно использовать два вида отступов:
open class func flexible(_ flexibleSpacing: CGFloat) -> Self // i.e. >= open class func fixed(_ fixedSpacing: CGFloat) -> Self // i.e. ==
flexible — позволяет заполнить свободное место, которое осталось в этой группе пространством.
fixed — позволяет задать фиксированный отступ.
В качестве примера я приведу Layout, в котором с помощью edgeSpacing расположил два столбца элементов посредине:
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.4),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil,
trailing: .flexible(16), bottom: nil)
let itemSize2 = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.4),
heightDimension: .fractionalHeight(1))
let item2 = NSCollectionLayoutItem(layoutSize: itemSize2)
item2.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: nil,
trailing: .flexible(0), bottom: nil)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(60))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item, item2])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}

NSCollectionLayoutGroup
NSCollectionLayoutGroup расширяет NSCollectionLayoutItem, добавляя возможность хранить несколько элементов, группируя их. У секции может быть только одна группа. Количество групп в Layout определяется количеством элементов в секции, которые отдает datasource. То есть, допустим, у нас есть одна группа, в которой находится один элемент, datasource говорит нам, что в секции десять элементов, значит, будет отрисовано десять групп. Если элементов в группе два, будет пять групп. Группы могут быть вертикальными и горизонтальными, хранить несколько элементов одного типа или разных типов. Задавать расстояние между элементами в группе можно с помощью переменной interItemSpacing.
open class func horizontal(layoutSize: NSCollectionLayoutSize, subitem: NSCollectionLayoutItem, count: Int) -> Self
open class func horizontal(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self
open class func vertical(layoutSize: NSCollectionLayoutSize, subitem: NSCollectionLayoutItem, count: Int) -> Self
open class func vertical(layoutSize: NSCollectionLayoutSize, subitems: [NSCollectionLayoutItem]) -> Self
open var interItemSpacing: NSCollectionLayoutSpacing?
Если вас не устраивают вертикальные и горизонтальные группы, тогда можно создать свой собственный вариант:
open class NSCollectionLayoutGroupCustomItem : NSObject, NSCopying {
public convenience init(frame: CGRect)
public convenience init(frame: CGRect, zIndex: Int)
open var frame: CGRect { get }
open var zIndex: Int { get }
}
public typealias NSCollectionLayoutGroupCustomItemProvider = (NSCollectionLayoutEnvironment) -> [NSCollectionLayoutGroupCustomItem]
open class func custom(layoutSize: NSCollectionLayoutSize, itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider) -> Self
NSCollectionLayoutGroupCustomItem — это простой объект, в котором нужно установить позицию объекта относительно контейнера. В блоке кода NSCollectionLayoutGroupCustomItemProvider на вход нам придет NSCollectionLayoutEnvironment, который содержит размер контейнера, а также его UITraitCollection. Более подробно мы рассмотрим этот класс чуть позже.
Самая главная особенность группы — это то, что мы можем помещать туда другие группы, как будто они являются элементами. Благодаря этому свойству я создал сложный Layout из четырех групп.
private func createLayout() -> UICollectionViewLayout {
let verticalItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(0.3))
let verticalItem = NSCollectionLayoutItem(layoutSize: verticalItemSize)
let verticalGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1))
let verticalGroup = NSCollectionLayoutGroup.vertical(layoutSize: verticalGroupSize,
subitem: verticalItem, count: 3)
verticalGroup.interItemSpacing = .fixed(8)
// ---------------------------------------------------------------------------------
let horizontalItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25),
heightDimension: .fractionalHeight(1))
let horizontalItem = NSCollectionLayoutItem(layoutSize: horizontalItemSize)
let horizontalItemSize2 = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.4),
heightDimension: .fractionalHeight(1))
let horizontalItem2 = NSCollectionLayoutItem(layoutSize: horizontalItemSize2)
let horizontalGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(0.3))
let horizontalGroup = NSCollectionLayoutGroup.horizontal(layoutSize: horizontalGroupSize,
subitems: [horizontalItem, horizontalItem2, horizontalItem])
let horizontalGroup2 = NSCollectionLayoutGroup.horizontal(layoutSize: horizontalGroupSize,
subitems: [horizontalItem2, horizontalItem, horizontalItem])
let horizontalGroup3 = NSCollectionLayoutGroup.horizontal(layoutSize: horizontalGroupSize,
subitems: [horizontalItem, horizontalItem, horizontalItem2])
horizontalGroup.interItemSpacing = .fixed(8)
horizontalGroup2.interItemSpacing = .fixed(8)
horizontalGroup3.interItemSpacing = .fixed(8)
// ---------------------------------------------------------------------------------
let horizontalsGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.75),
heightDimension: .fractionalHeight(1))
let horizontalsGroup = NSCollectionLayoutGroup.vertical(layoutSize: horizontalsGroupSize,
subitems: [horizontalGroup, horizontalGroup2, horizontalGroup3])
horizontalsGroup.interItemSpacing = .flexible(0)
// ---------------------------------------------------------------------------------
let finalGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5))
let finalGroup = NSCollectionLayoutGroup.horizontal(layoutSize: finalGroupSize,
subitems: [horizontalsGroup, verticalGroup])
let section = NSCollectionLayoutSection(group: finalGroup)
section.interGroupSpacing = 8
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}

NSCollectionLayoutSection
После разбора всех предыдущих составных частей секции здесь уже практически не должно оставаться вопросов. Секция состоит из повторяющихся групп, в которых расположены элементы. Она также может задавать отступы для контента и между группами:
open var contentInsets: NSDirectionalEdgeInsets open var interGroupSpacing: CGFloat
Кроме этого, у нее есть два интересных свойства:
open var orthogonalScrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior open var visibleItemsInvalidationHandler: NSCollectionLayoutSectionVisibleItemsInvalidationHandler? public typealias NSCollectionLayoutSectionVisibleItemsInvalidationHandler = ([NSCollectionLayoutVisibleItem], CGPoint, NSCollectionLayoutEnvironment) -> Void
visibleItemsInvalidationHandler — это блок кода, который вызывается перед прорисовкой элементов. Как его использовать, я пока не нашел. Как вариант, можно всегда знать, какие элементы на экране, и знать offset текущего списка. При необходимости можно поменять свойства элементов (frame, alpha, zIndex...).
orthogonalScrollingBehavior — с помощью этой переменной можно задать скроллинг секции в противоположную сторону относительно коллекции. Если вы пытались сделать это раньше, то самым простым решением было положить в ячейку еще одну коллекцию. Но с точки зрения архитектуры это всегда выглядело ужасно.
Есть 5 вариантов скролла:
// Standard scroll view behavior: UIScrollViewDecelerationRateNormal
case continuous
// Scrolling will come to rest on the leading edge of a group boundary
case continuousGroupLeadingBoundary
// Standard scroll view paging behavior (UIScrollViewDecelerationRateFast) with page size == extent of the collection view's bounds
case paging
// Fractional size paging behavior determined by the sections layout group's dimension
case groupPaging
// Same of group paging with additional leading and trailing content insets to center each group's contents along the orthogonal axis
case groupPagingCentered
Вот пример кода с continuous-типом:
private func listSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
return NSCollectionLayoutSection(group: group)
}
private func gridSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item, count: 3)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return section
}
private func createLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { sectionNumber, env -> NSCollectionLayoutSection? in
switch Section(rawValue: sectionNumber) {
case .main:
return self.listSection()
case .second:
return self.gridSection()
default:
return nil
}
}
}

UICollectionViewCompositionalLayout
UICollectionViewCompositionalLayout — основной класс, который инициализируется двумя вариантами: первый — просто секцией, второй — через блок, который будет вызываться каждый раз при отрисовке секции или смене размера контейнера.
public init(section: NSCollectionLayoutSection)
public init(section: NSCollectionLayoutSection, configuration: UICollectionViewCompositionalLayoutConfiguration)
public typealias UICollectionViewCompositionalLayoutSectionProvider = (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection?
public init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider)
public init(sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider, configuration: UICollectionViewCompositionalLayoutConfiguration)
Второй вариант будет интересен для тех, кто хочет задавать разное отображение элементов для вертикального и горизонтального режимов или разное отображение секций, как в примере выше. В UICollectionViewCompositionalLayoutConfiguration можно установить направление скролла и расстояние между секциями.
NSCollectionLayoutSupplementaryItem, NSCollectionLayoutBoundarySupplementaryItem, NSCollectionLayoutDecorationItem
Об этих дополнительных элементах я умалчивал сознательно, потому что в моей практике они используются не очень часто, но иногда бывают крайне полезными. Я вкратце пройдусь по каждому и покажу примеры их использования.
NSCollectionLayoutSupplementaryItem может использоваться для элемента и группы, он наследуется от NSCollectionLayoutItem. Основное их отличие от элементов — это то, как их располагают в Layout. За это отвечает отдельный класс NSCollectionLayoutAnchor. Указанный элемент располагается поверх основного, поэтому мы можем привязать его к сторонам элемента с помощью этого класса. Звучит немного страшно, но на практике легко, вот пример из документации.
// +------------------+ +------+ +------------------+
// | [.top, .leading] | |[.top]| | [.top,.trailing] |
// +--+---------------+ +---+--+ +---------------+--+
// | | |
// v v v
// +-----+----------------+-----+----------------+-----+
// |~~~~~| |~~~~~| |~~~~~|
// |~~~~~| |~~~~~| |~~~~~|
// +-----+ +-----+ +-----+
// | |
// +-----+ +-----+
// +--------------+ |~~~~~| |~~~~~| +-------------+
// | [.leading] |--->|~~~~~| |~~~~~|<---| [.trailing] |
// +--------------+ +-----+ +-----+ +-------------+
// | |
// +-----+ +-----+ +-----+
// |~~~~~| |~~~~~| |~~~~~|
// |~~~~~| |~~~~~| |~~~~~|
// +-----+----------------+-----+----------------+-----+
// ^ ^ ^
// | | |
// +---+---------------+ +----+----+ +--------------+----+
// |[.bottom, .leading]| |[.bottom]| |[.bottom,.trailing]|
// +-------------------+ +---------+ +-------------------+
//
// Edges are specified as shown above.
public convenience init(edges: NSDirectionalRectEdge)
К этому расположению мы можем добавить offset.
public convenience init(edges: NSDirectionalRectEdge, absoluteOffset: CGPoint) public convenience init(edges: NSDirectionalRectEdge, fractionalOffset: CGPoint)
А вот и пример, в котором я прикрепил к группе дополнительную галочку:
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8,
bottom: 8, trailing: 8)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.2))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item, count: 3)
let suppItemSize = NSCollectionLayoutSize(widthDimension: .absolute(45),
heightDimension: .absolute(45))
let suppItemPlace = NSCollectionLayoutAnchor(edges: [.top, .trailing])
let suppItem = NSCollectionLayoutSupplementaryItem(layoutSize: suppItemSize,
elementKind: CheckmarkGridViewController.checkMarkElementKind,
containerAnchor: suppItemPlace)
group.supplementaryItems = [suppItem]
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}

NSCollectionLayoutBoundarySupplementaryItem — это всем уже знакомые футеры и хедеры. Эти элементы можно добавить к NSCollectionLayoutSection и UICollectionViewCompositionalLayoutConfiguration. Размер задается уже известным нам NSCollectionLayoutSize, расположение — NSRectAlignment. С помощью расположения вы говорите Layout, что это будет: футер (.bottom) или хедер (.top).
public convenience init(layoutSize: NSCollectionLayoutSize, elementKind: String, alignment: NSRectAlignment)
public convenience init(layoutSize: NSCollectionLayoutSize, elementKind: String, alignment: NSRectAlignment, absoluteOffset: CGPoint)
Самая интересная переменная в этом классе — pinToVisibleBounds, которая позволяет реализовать sticky header без особых трудностей. Если ее поставить в true, тогда текущий элемент будет видимым, пока видна секция.
В примере я создал три таких элемента: сверху, слева и снизу. Примечательно, что относительный размер задается относительно контейнера, а не секции, поэтому левому элементу нельзя задать размер, не посчитав размер секции самостоятельно.
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9),
heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0),
top: nil,
trailing: nil,
bottom: nil)
let footerHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(50.0))
let leftSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.1),
heightDimension: .absolute(150.0))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerHeaderSize,
elementKind: HeaderFooterViewController.headerKind,
alignment: .top)
let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerHeaderSize,
elementKind: HeaderFooterViewController.footerKind,
alignment: .bottom)
let left = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: leftSize,
elementKind: HeaderFooterViewController.leadingKind,
alignment: .leading)
let section = NSCollectionLayoutSection(group: group)
section.boundarySupplementaryItems = [header, footer, left]
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 16
let layout = UICollectionViewCompositionalLayout(section: section, configuration: config)
return layout
}

NSCollectionLayoutDecorationItem позволяет установить задний фон для секции, у него присутствует один статический инициализатор:
open class func background(elementKind: String) -> Self
Так как этот элемент наследуется от NSCollectionLayoutItem, то ему можно задать contentInsets. Обычно это необходимо для того, чтобы выровнять границы фона. Единственное, что отличает его от остальных дополнительных элементов, — это регистрация класса для отображения: его нужно регистрировать в Layout.
В примере я покажу, как сделать тень по всей секции:
private func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitems: [item])
group.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(0), top: nil, trailing: nil, bottom: nil)
let background = NSCollectionLayoutDecorationItem.background(elementKind: "background")
background.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
let section = NSCollectionLayoutSection(group: group)
section.decorationItems = [background]
section.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)
let layout = UICollectionViewCompositionalLayout(section: section)
layout.register(BackgroundView.self, forDecorationViewOfKind: "background")
return layout
}

Итоги
UICollectionViewCompositionalLayout — это еще один эволюционный шаг в разработке на iOS. Благодаря этому инструменту создавать коллекции элементов стало намного проще. Декларативный подход позволяет удовлетворить 99% пожеланий разработчиков. Учитывая, что SwiftUI еще сырой и в нем совсем нет коллекций, я настойчиво рекомендую взять этот инструмент на вооружение в новых проектах. В следующей статье я постараюсь разобраться во втором не менее важном инструменте — UICollectionViewDiffableDataSource.
- Документация.
- Advances in Collection View Layout (WWDC).
- Using Collection View Compositional Layouts and Diffable Data Sources.
- GitHub.