Відображення списків з допомогою 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 group containing
 open class func fractionalWidth(_ fractionalWidth: CGFloat) -> Self

 //dimension is computed as a fraction of the height of the group containing
 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 frame var: 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 axis orthogonal
 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 Layout View (WWDC) .
  3. Using Collection View Compositional Layouts and Diffable Data Sources .
  4. GitHub .

Опубліковано: 03/09/19 @ 10:00
Розділ Різне

Рекомендуємо:

Потрібні програмісту алгоритми і структури даних
iOS дайджест #33: Special — SwiftUI
Як змусити Amazon Alexa грати музику з Google Music, хоч вона цього й не хоче
5 кращих книг для вивчення JavaScript від Senior Front-end розробника Олександра Головатого
Роль Product Manager на різних етапах розвитку проекту