.NET Fest: полная программа конференции на сайте. Присоединяйся к самому большому .NET ивенту
×Закрыть

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

4 комментария

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Спасибо за статью! Напомнило о старом и никому не нужном Windows Phone.

Было бы круто, если бы еще допилили группировки элементов по типу как KTCenterFlowLayout

Подписаться на комментарии