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

  1. Документация.
  2. Advances in Collection View Layout (WWDC).
  3. Using Collection View Compositional Layouts and Diffable Data Sources.
  4. GitHub.
Похожие статьи:
16 липня починають діяти нові норми Закону України «Про забезпечення функціонування української мови як державної». Вони...
Квантова компанія Haiqu увійшла до переліку найперспективніших європейських стартапів від видання Sifted. До рейтингу також...
Міністерство оборони нарощує можливості з залучення технологій, виробництва та прямих постачань дронів. Сьогодні...
12 марта в Киеве стартует новый курс Brain Academy — «React.js: от основ до масштабируемых SPA»!Курс будет полезен для Frontend...
Компания LEXAND представила новую линейку полноразмерных мобильных телефонов, отличающихся, со слов...
Яндекс.Метрика