How does compositional layout describe a collection view, and how do you read its output?

7.5.1 · L3 · Interview

Predict first

Where does each of four items land in a two-column group (fractional width 0.5, height 50) in a 400-point collection? (Printed as x,y,width.)

import UIKit

let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(50)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let cv = UICollectionView(frame: CGRect(x: 0, y: 0, width: 400, height: 400), collectionViewLayout: UICollectionViewCompositionalLayout(section: section))
let reg = UICollectionView.CellRegistration<UICollectionViewCell, Int> { _, _, _ in }
let ds = UICollectionViewDiffableDataSource<Int, Int>(collectionView: cv) { cv, ip, id in cv.dequeueConfiguredReusableCell(using: reg, for: ip, item: id) }
var snap = NSDiffableDataSourceSnapshot<Int, Int>()
snap.appendSections([0])
snap.appendItems(Array(0..<4))
ds.apply(snap, animatingDifferences: false)
cv.layoutIfNeeded()
print((0..<4).map { i -> String in
    let f = cv.layoutAttributesForItem(at: IndexPath(item: i, section: 0))!.frame
    return "\(Int(f.minX)),\(Int(f.minY)),\(Int(f.width))"
})
  • 0,0,200 200,0,200 0,50,200 200,50,200
  • 0,0,400 0,50,400 0,100,400 0,150,400
  • 0,0,200 200,0,200 400,0,200 600,0,200

Predict first

What changes with 5-point item insets, 20-point section insets, 10 top and bottom, and 8 between rows?

import UIKit

let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(50)))
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)
section.interGroupSpacing = 8
let cv = UICollectionView(frame: CGRect(x: 0, y: 0, width: 400, height: 400), collectionViewLayout: UICollectionViewCompositionalLayout(section: section))
let reg = UICollectionView.CellRegistration<UICollectionViewCell, Int> { _, _, _ in }
let ds = UICollectionViewDiffableDataSource<Int, Int>(collectionView: cv) { cv, ip, id in cv.dequeueConfiguredReusableCell(using: reg, for: ip, item: id) }
var snap = NSDiffableDataSourceSnapshot<Int, Int>()
snap.appendSections([0])
snap.appendItems(Array(0..<4))
ds.apply(snap, animatingDifferences: false)
cv.layoutIfNeeded()
print((0..<4).map { i -> String in
    let f = cv.layoutAttributesForItem(at: IndexPath(item: i, section: 0))!.frame
    return "\(Int(f.minX)),\(Int(f.minY)),\(Int(f.width))"
})
  • 25,10,170 205,10,170 25,68,170 205,68,170
  • 0,10,200 200,10,200 0,68,200 200,68,200

Mental model. Item, group, section are boxes inside boxes. You say how big each is relative to the one around it, and UIKit does the measuring.

UICollectionViewCompositionalLayout builds a layout from three nested pieces. An item (NSCollectionLayoutItem) describes one cell’s size. A group arranges items in a row, a column or a custom pattern and has its own size. A section stacks groups and adds insets, spacing, headers and footers, and scrolling behaviour. Sizes are dimensions: .fractionalWidth (a fraction of the container), .absolute (points), or .estimated (a guess the system corrects after self-sizing). A group of two items each .fractionalWidth(0.5) gives two columns. Per-section differences come from the closure form, UICollectionViewCompositionalLayout { sectionIndex, environment in … }, whose environment exposes the container size and trait collection so one layout can adapt to compact and regular widths.

A few details decide the output. An item’s contentInsets shrink the drawn frame inside the space it was given, so a gap between cells comes from insets, not from the item’s size. A section’s contentInsets reduce the width fractions are measured against. interGroupSpacing spaces rows. Features that used to need custom layouts are built in: orthogonalScrollingBehavior makes a section scroll sideways inside a vertical collection (carousels), supplementary items add headers and badges, and decoration items add backgrounds. Prefer it to UICollectionViewFlowLayout for anything beyond a uniform grid.

Each item takes half of the group’s width, so two fit per row, and the group’s height of 50 puts the next row at y = 50.

The group is now 360 wide, so each item cell is 180, and the 5-point insets leave a 170-wide frame starting at x = 25. The second row starts at 10 + 50 + 8 = 68.

Common traps

  • Making gaps with item sizes. Use contentInsets or spacing; changing sizes breaks the fractions.
  • Forgetting that section insets shrink what fractions refer to. A 0.5 item in a 400-wide collection with 20-point insets is 180 wide, not 200.

They might follow up with

  • How do you build a carousel section? Set section.orthogonalScrollingBehavior = .continuous or .groupPaging.
  • When is .estimated needed? For content whose height depends on its text; the cell then sizes itself.

Related questions

  • 7.5.2 How does a diffable data source work, and what does it require of your item identifiers?
  • 7.5.4 How do prefetching and self-sizing cells keep scrolling smooth, and what can still make it janky?
  • 7.7.1 What is a trait collection, and how do size classes and trait changes drive adaptive layout?

How does a diffable data source work, and what does it require of… →