UICollectionView CompositionalLayout Part 1: Introduction
Learn the basics of UICollectionViewCompositionalLayout before diving in and making your own layouts
UICollectionViewCompositionalLayout was introduced at WWDC in 2019. It is a flexible way to very simply create all kinds of collection view layouts. Let’s take a high-level look at how it works.
UICollectionViewCompositionalLayout provides a simple way to make all kinds of complex layouts. It was used to make the layout for the App Store and the layout shown above.
While complex, both layouts can be set up fairly simply using UICollectionViewCompositionalLayout because of how things are structured in this new system. In UICollectionViewCompositionalLayout we use 3 levels of building blocks to construct our layouts. From smallest to largest they are: items, groups, sections. Each level holds elements of the level below it. As shown, items (purple) go in groups (green), groups go in sections (gray) and sections make our layout (blue).
Items correspond with cells in a collection view and sections are just that, sections, so no change there. Groups are new with UICollectionViewCompositionalLayout and this new intermediate level is what provides us with so much power.
Before we get into the code to actually build a layout with UICollectionViewCompositionalLayout we need to understand how sizing works. Sizes for objects in compositional layouts can be determined in three ways. They can be set with an absolute value, a relative value or with an estimated value. Sizes are set using NSCollectionLayoutSize objects that have just 2 properties - a width and a height - both of which are NSCollectionLayoutDimension'92s.
Here we see a cell (item) set using absolute values. Its width is set to 100 points and its height is set to 50 points.
We use the fractionalWidth and fractionalHeight functions to specify relative values for sizes. Each function takes a CGFloat as a parameter. This CGFloat is a number 0.0 to 1.0 that says how much of its containers corresponding dimension the item will take up. Below we have a group with an absolute width of 200 points and an absolute height of 50 points. Inside of this group we have an item with an absolute width of 200 points and a relative height of 0.8. This results in the item have a height of 40 points because 40 is equal to the fractional height value (0.8) times its containers height value (50).
If we give an item or group an estimated height, of 125 for example, the layout system will use this value as a starting point but will calculate whatever value fits best in the layout at run time.
Now that you understand how compositional layouts are built using items, groups and sections and how sizing is determined, you are ready to put this knowledge into action and build your first UICollectionViewCompositionalLayout. Check out the next tip in this series to see exactly how!
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
enum Section: Int, CaseIterable {
case first, second, third, fourth
var sectionLayout: NSCollectionLayoutSection {
switch self {
case .first: return self.section1()
case .second: return self.section2()
case .third: return self.section3()
case .fourth: return self.section4()
}
}
var color: UIColor {
switch self {
case .first: return .red
case .second: return .purple
case .third: return .green
case .fourth: return .orange
}
}
private func section1() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50.0)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
private func section2() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50.0)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
private func section3() -> NSCollectionLayoutSection {
let columnItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5)))
columnItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let stackedColumnGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0)), subitem: columnItem, count: 2)
let tallItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0)))
tallItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let combinedGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(2/3)), subitems: [stackedColumnGroup, tallItem])
let section = NSCollectionLayoutSection(group: combinedGroup)
section.orthogonalScrollingBehavior = .paging
return section
}
private func section4() -> NSCollectionLayoutSection {
let rowItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0)))
rowItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let rowGroup = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1/2)), subitems: [rowItem])
let stackedRowGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(2/3)), subitems: [rowGroup])
let wideItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1/3)))
wideItem.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let combinedGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(3/4)), subitems: [stackedRowGroup, wideItem])
let section = NSCollectionLayoutSection(group: combinedGroup)
section.orthogonalScrollingBehavior = .paging
return section
}
}
override func loadView() {
let view = UIView()
self.view = view
setUpCollectionView()
}
func setUpCollectionView() {
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let section = Section(rawValue: sectionIndex)!
return section.sectionLayout
})
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .white
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dataSource = self
view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.heightAnchor.constraint(equalToConstant: 400.0).isActive = true
collectionView.widthAnchor.constraint(equalToConstant: 200.0).isActive = true
}
}
extension MyViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return Section.allCases.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.resuseIdentifier, for: indexPath) as! Cell
cell.label.text = "(\(indexPath.section), \(indexPath.row))"
cell.backgroundColor = Section(rawValue: indexPath.section)!.color
return cell
}
}
class Cell: UICollectionViewCell {
static let resuseIdentifier = "Cell"
let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
label.topAnchor.constraint(equalTo: topAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
PlaygroundPage.current.liveView = MyViewController()
* This article originally appeared in the ZipTips app.
Want to connect?
Contact me to talk about this article, working together or something else.