UICollectionView Layouts in Depth
UICollectionView
in UIKit relies on UICollectionViewLayout
to define how items, supplementary views (e.g., headers, footers), and decorations are arranged. This document provides an in-depth exploration of three key layout types: UICollectionViewFlowLayout
, UICollectionViewCompositionalLayout
, and custom UICollectionViewLayout
subclasses. Each section includes detailed explanations, properties, methods, examples, and best practices, focusing on their capabilities, use cases, and implementation.
Overview of UICollectionViewLayout
UICollectionViewLayout
is an abstract base class responsible for calculating the positions and attributes of items and views in a UICollectionView
. Subclasses define specific layouts, and layouts can be swapped dynamically using collectionView.setCollectionViewLayout(_:animated:)
.
Key Responsibilities
- Prepare Layout: Compute layout attributes (e.g., positions, sizes) in
prepare()
. - Provide Attributes: Return attributes for items and views via methods like
layoutAttributesForElements(in:)
. - Handle Updates: Support insertions, deletions, and animations.
- Define Content Size: Specify the collection view’s scrollable area via
collectionViewContentSize
.
Common Subclasses
UICollectionViewFlowLayout
: Grid-based, linear layout.UICollectionViewCompositionalLayout
: Modern, flexible, section-based layout (iOS 13+).- Custom
UICollectionViewLayout
: Fully custom layouts for unique designs.
1. UICollectionViewFlowLayout
UICollectionViewFlowLayout
is a concrete subclass of UICollectionViewLayout
that arranges items in a grid or line, with customizable scroll direction, spacing, and sizes. It’s ideal for simple, uniform layouts like photo galleries or lists.
Key Properties
Property | Type | Description |
---|---|---|
scrollDirection | UICollectionView.ScrollDirection | Scroll direction (.vertical or .horizontal ). |
itemSize | CGSize | Default size for all items (overridable via delegate). |
minimumLineSpacing | CGFloat | Minimum spacing between lines (rows or columns). |
minimumInteritemSpacing | CGFloat | Minimum spacing between items in the same line. |
sectionInset | UIEdgeInsets | Padding around each section. |
headerReferenceSize | CGSize | Size for section headers. |
footerReferenceSize | CGSize | Size for section footers. |
Key Delegate Methods (UICollectionViewDelegateFlowLayout)
Method | Description |
---|---|
collectionView(_:layout:sizeForItemAt:) | Returns custom size for an item. |
collectionView(_:layout:insetForSectionAt:) | Returns custom insets for a section. |
collectionView(_:layout:minimumLineSpacingForSectionAt:) | Returns custom line spacing for a section. |
collectionView(_:layout:minimumInteritemSpacingForSectionAt:) | Returns custom inter-item spacing. |
collectionView(_:layout:referenceSizeForHeaderInSection:) | Returns custom header size. |
Example: Grid Layout with FlowLayout
This example creates a vertical grid with two items per row, headers, and custom spacing.
import UIKit
import SnapKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private let collectionView: UICollectionView
private let data = (1...20).map { "Item \($0)" }
override init(nibName: String?, bundle: Bundle?) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
super.init(nibName: nibName, bundle: bundle)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
private func setupCollectionView() {
collectionView.backgroundColor = .systemBackground
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.identifier)
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
// DataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count / 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = .systemBlue
let label = UILabel(frame: cell.contentView.bounds)
label.text = data[indexPath.item + (indexPath.section * data.count / 2)]
label.textAlignment = .center
label.textColor = .white
cell.contentView.addSubview(label)
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.identifier, for: indexPath) as! HeaderView
header.configure(with: indexPath.section == 0 ? "Section 1" : "Section 2")
return header
}
// DelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.frame.width - 30) / 2 // 2 items per row, accounting for insets and spacing
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 50)
}
}
class HeaderView: UICollectionReusableView {
static let identifier = "Header"
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.font = .systemFont(ofSize: 18, weight: .bold)
addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalTo(self).inset(8)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with text: String) {
label.text = text
}
}
Features and Limitations
- Features: Easy to use, supports headers/footers, customizable via delegate, supports both vertical and horizontal scrolling.
- Limitations: Limited to grid-like layouts, less flexible for complex or adaptive designs, delegate-based customization can become verbose.
- Use Case: Simple grids or lists (e.g., photo galleries, product catalogs).
Best Practices
- Use delegate methods for dynamic sizing to support different device sizes.
- Set
estimatedItemSize
for self-sizing cells with Auto Layout. - Avoid complex calculations in delegate methods to maintain performance.
2. UICollectionViewCompositionalLayout
Introduced in iOS 13, UICollectionViewCompositionalLayout
is a modern, declarative layout system that builds layouts using composable sections, groups, and items. It’s highly flexible, supporting adaptive layouts, complex arrangements, and per-section customization.
Core Concepts
- Item: Represents a single cell (
NSCollectionLayoutItem
). - Group: A collection of items arranged horizontally, vertically, or custom (
NSCollectionLayoutGroup
). - Section: A collection of groups with insets, spacing, and supplementary views (
NSCollectionLayoutSection
). - Layout: Combines sections, defined via a closure or configuration (
UICollectionViewCompositionalLayout
).
Key Classes
Class | Description |
---|---|
NSCollectionLayoutSize | Defines width and height dimensions (e.g., .absolute , .fractionalWidth , .estimated ). |
NSCollectionLayoutItem | Represents a cell with size and edge spacing. |
NSCollectionLayoutGroup | Arranges items in a horizontal, vertical, or custom layout. |
NSCollectionLayoutSection | Defines a section with groups, insets, and supplementary views. |
NSCollectionLayoutBoundarySupplementaryItem | Represents headers or footers. |
UICollectionViewCompositionalLayout | The layout object, initialized with a section provider or configuration. |
Key Properties and Methods
- Section Provider: A closure
(_: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection?
to define sections dynamically. configuration
: AUICollectionViewCompositionalLayoutConfiguration
for global settings (e.g., scroll direction, inter-section spacing).
Example: Mixed Layout with CompositionalLayout
This example creates a layout with two sections: a horizontal scrolling list and a vertical grid, with headers.
import UIKit
import SnapKit
class ViewController: UIViewController, UICollectionViewDataSource {
private var collectionView: UICollectionView!
private let data = (1...20).map { "Item \($0)" }
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
private func setupCollectionView() {
view.backgroundColor = .systemBackground
let layout = createLayout()
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .systemBackground
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.identifier)
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
private func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (sectionIndex: Int, _ layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
switch sectionIndex {
case 0:
// Horizontal scrolling list
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(150), heightDimension: .absolute(100))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(150), heightDimension: .absolute(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.interGroupSpacing = 10
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
case 1:
// Vertical scrolling grid
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(100))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .fixed(10)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
default:
return nil
}
}
}
// DataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count / 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = .systemBlue
let label = UILabel(frame: cell.contentView.bounds)
label.text = data[indexPath.item + (indexPath.section * data.count / 2)]
label.textAlignment = .center
label.textColor = .white
cell.contentView.addSubview(label)
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.identifier, for: indexPath) as! HeaderView
header.configure(with: indexPath.section == 0 ? "Horizontal List" : "Grid")
return header
}
}
class HeaderView: UICollectionReusableView {
static let identifier = "Header"
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.font = .systemFont(ofSize: 18, weight: .bold)
addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalTo(self).inset(8)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with text: String) {
label.text = text
}
}
Features and Limitations
- Features: Declarative API, per-section layouts, adaptive sizing, built-in animations, supports orthogonal scrolling, easy header/footer integration.
- Limitations: Requires iOS 13+, steeper learning curve for complex layouts may require more code than FlowLayout for simple grids.
- Use Case: Modern apps with varied or adaptive layouts (e.g., App Store, settings apps).
Best Practices
- Use
layoutEnvironment
to adapt layouts to container size or trait changes. - Leverage
.estimated
dimensions for self-sizing cells with Auto Layout. - Reuse groups and items across sections to reduce code duplication.
- Combine with
UICollectionViewDiffableDataSource
for seamless data updates.
3. Custom UICollectionViewLayout
A custom UICollectionViewLayout
subclass provides complete control over item placement, ideal for unique layouts like circular, radial arrangements, or physics-based layouts. It requires implementing key methods to compute layout attributes manually.
Key Methods to Override
Method | Description |
---|---|
prepare() | Called to compute and cache layout attributes. |
collectionViewContentSize() | Returns the scrollable content size. |
layoutAttributesForElements(in:) | Returns attributes for items/supplementary views in a rect. |
layoutAttributesForItem(at:) | Returns attributes for a specific item. |
layoutAttributesForSupplementaryView(ofKind:at:) | Returns attributes for a supplementary view. |
shouldInvalidateLayout(forBoundsChange:) | Returns whether to recompute layout on bounds changes. |
Example: Circular Layout
This example creates a circular arrangement of items around the collection view’s center.
import UIKit
class CircularLayout: UICollectionViewLayout {
private var attributesCache: [IndexPath: UICollectionViewLayoutAttributes] = [:]
private let itemSize = CGSize(width: 50, height: 50)
private let radius: CGFloat = 100
override func prepare() {
super.prepare()
attributesCache.removeAll()
guard let collectionView = collectionView else { return }
let center = CGPoint(x: collectionView.bounds.width / 2, y: collectionView.bounds.height / 2)
let itemCount = collectionView.numberOfItems(inSection: 0)
for item in 0..<itemCount {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = CGFloat(item) * (2 * .pi / CGFloat(itemCount))
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
attributes.frame = CGRect(x: x - itemSize.width / 2, y: y - itemSize.height / 2, width: itemSize.width, height: itemSize.height)
attributesCache[indexPath] = attributes
}
}
override var collectionViewContentSize: CGSize {
guard let collectionView = collectionView else { return .zero }
return collectionView.bounds.size
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesCache.values.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return attributesCache[indexPath]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
class ViewController: UIViewController, UICollectionViewDataSource {
private let collectionView: UICollectionView
init() {
let layout = CircularLayout()
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
private func setupCollectionView() {
view.backgroundColor = .systemBackground
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
}
// DataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 8
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = .systemBlue
let label = UILabel(frame: cell.contentView.bounds)
label.text = "Item \(indexPath.item + 1)"
label.textAlignment = .center
label.textColor = .white
cell.contentView.addSubview(label)
return cell
}
}
Features and Challenges
- Features: Unlimited layout possibilities, fine-grained control over item placement, supports animations via custom attributes.
- Challenges: Complex to implement, requires manual attribute calculations, potential performance overhead for large datasets.
- Use Case: Unique, non-standard layouts (e.g., circular galleries, spiral layouts, physics-based layouts).
Best Practices
- Cache attributes in
prepare()
to avoid redundant calculations. - Optimize
layoutAttributesForElements(in: to
rect` by filtering cached attributes. - Handle dynamic updates (e.g.,
prepareForCollectionViewUpdates
) for insertions/deletions. - Test on various device sizes and orientations to ensure responsiveness.
Comparison of Layouts
Aspect | FlowLayout | CompositionalLayout | Custom Layout |
---|---|---|---|
Ease of Use | Simple, delegate-based | Declarative, moderate learning curve | Complex, manual implementation |
Flexibility | Limited to grids/lists | Highly flexible, section-based layouts | Unlimited, fully custom |
Performance | Good for simple layouts | Optimized for modern apps | Depends on implementation |
Minimum iOS Version | iOS 6+ | iOS 13+ | iOS 14+ |
Use Case | Simple grids, legacy apps | Modern, adaptive layouts | Unique, non-standard layouts |
Animations | Basic animations | Built-in, smooth animations | Customizable, manual control |
Best Practices for All Layouts
- Performance: Avoid heavy computations in layout methods; cache where possible.
- Accessibility: Ensure layouts support dynamic type and VoiceOver navigation.
- Testing: Test layouts on multiple devices, orientations, and content sizes.
- Auto Layout: Use constraints in cells for dynamic sizing.
- Diffable Data Source: Pair with
UICollectionViewDiffableDataSource
for modern apps to handle data updates. - Animation: Use
performBatchUpdates
or diffable snapshots for smooth transitions.
Troubleshooting
- Layout Not Rendering: Verify
prepare()
andlayoutAttributesForElements(in:)
implementations. - Incorrect Sizing: Check item sizes, insets, and delegate methods (FlowLayout) or size dimensions (CompositionalLayout).
- Performance Issues: Profile with Instruments to identify bottlenecks in attribute calculations.
- Animation Glitches: Ensure
invalidateLayout()
is called appropriately for bounds changes. - Supplementary Views Missing: Register and configure headers/footers correctly.