Отображение списков с помощью 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.