Swift By Rahul

Compositional Layout in UICollectionView

UICollectionView has been a cornerstone of iOS development for displaying ordered data sets in customizable layouts. From simple lists to complex grids, it has empowered developers to create rich, dynamic interfaces. However, achieving truly intricate, adaptive layouts—especially those mixing different visual styles within a single collection view—often required significant boilerplate code, custom UICollectionViewLayout subclasses, or even multiple collection views.

Enter UICollectionViewCompositionalLayout. Introduced in iOS 13, this powerful declarative layout system revolutionized how we approach collection view layouts. It provides a flexible and intuitive way to describe your layout visually, empowering you to build highly complex and adaptive UIs with significantly less effort than ever before. If you've ever struggled with UICollectionViewFlowLayout's limitations or felt overwhelmed by custom layout subclassing, CompositionalLayout is here to be your new best friend.

This article will guide you through the core concepts of UICollectionViewCompositionalLayout, demonstrating how to construct various layouts, from simple lists to more complex, multi-section designs, all while keeping your code clean and maintainable.

The Evolution of UICollectionView Layouts

Historically, UICollectionViewFlowLayout was the go-to choice for UICollectionView. It's excellent for grid-based or line-based layouts, but its "flow" nature meant that all items in a section generally followed the same pattern. If you wanted a section with a horizontal scrolling list and another section with a three-column grid, you were looking at either hacky workarounds, multiple collection views, or diving deep into custom UICollectionViewLayout subclasses—a path often fraught with challenges.

UICollectionViewCompositionalLayout changes this paradigm entirely. Instead of calculating frames directly, you compose your layout from smaller, reusable components: items, groups, and sections. This declarative approach allows you to describe what your layout should look like, rather than how to calculate every single coordinate.

UICollectionView Layout Evolution UICollectionViewFlowLayout Simple grids & lists Limited flexibility Introduced iOS 13 UICollectionViewCompositionalLayout Declarative, flexible, adaptive Complex & mixed layouts

Understanding the Building Blocks

UICollectionViewCompositionalLayout is built upon a hierarchy of four fundamental components:

  1. NSCollectionLayoutItem: The smallest unit. This represents a single cell in your collection view. You define its size relative to its container (the group).
  2. NSCollectionLayoutGroup: A container for one or more items. Groups define how items are arranged (e.g., horizontally, vertically, or custom). You define the group's size relative to its container (the section).
  3. NSCollectionLayoutSection: A container for one or more groups. Sections define the overall layout for a specific part of your collection view, including its content insets, inter-group spacing, and supplementary views (like headers/footers).
  4. UICollectionViewCompositionalLayout: The top-level object that takes one or more NSCollectionLayoutSection instances to define the complete layout for your UICollectionView.

This hierarchical structure allows for incredible flexibility. You define the layout from the inside out: item sizes are defined relative to their group, group sizes relative to their section, and section layouts make up the whole collection view layout.

Here's an ASCII diagram illustrating this hierarchy:

UICollectionViewCompositionalLayout
       │
       ▼
  NSCollectionLayoutSection
       │
       ▼
   NSCollectionLayoutGroup
       │
       ▼
   NSCollectionLayoutItem

Crafting Your First Compositional Layout: A Simple List

Let's start with a common scenario: a simple vertical list. Each item takes the full width and has a fixed height.

import UIKit

class ListViewController: UIViewController {

    enum Section {
        case main
    }

    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Int>!

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Simple List"
        configureHierarchy()
        configureDataSource()
    }

    private func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)

        // Register cell
        collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: "ListCell")
    }

    private func createLayout() -> UICollectionViewLayout {
        // Item: full width, fixed height
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .absolute(44))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // Group: vertical stack of items, full width, dynamic height
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .estimated(44)) // Estimated height for self-sizing cells
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

        // Section: contains the group
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        section.interGroupSpacing = 8 // Spacing between groups (items in this case)

        return UICollectionViewCompositionalLayout(section: section)
    }

    private func configureDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in

            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListCell", for: indexPath) as! UICollectionViewListCell
            var content = cell.defaultContentConfiguration()
            content.text = "Item \(identifier)"
            cell.contentConfiguration = content
            return cell
        }

        // Initial snapshot
        var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
        snapshot.appendSections([.main])
        snapshot.appendItems(Array(0..<30))
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

In this example:

  • itemSize: fractionalWidth(1.0) means the item will take 100% of its group's width. absolute(44) gives it a fixed height.
  • groupSize: fractionalWidth(1.0) means the group will take 100% of its section's width. estimated(44) is important for vertical groups containing items with absolute heights, or for self-sizing cells.
  • NSCollectionLayoutGroup.vertical: This convenience initializer creates a group that stacks its subitems vertically.
  • NSCollectionLayoutSection: The section holds our group. We add contentInsets and interGroupSpacing for visual appeal.
  • UICollectionViewCompositionalLayout(section: section): For a single-section layout, you can pass the section directly.

Adding Complexity: A Simple Grid Layout

Now, let's create a grid layout with multiple items per row. This is where CompositionalLayout really shines compared to FlowLayout for custom grid arrangements.

// ... (ListViewController structure, but for a new GridViewController)

class GridViewController: UIViewController {

    enum Section {
        case main
    }

    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Int>!

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Simple Grid"
        configureHierarchy()
        configureDataSource()
    }

    private func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createGridLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)

        collectionView.register(GridCell.self, forCellWithReuseIdentifier: GridCell.reuseIdentifier)
    }

    private func createGridLayout() -> UICollectionViewLayout {
        // Item: one-third width, full height of its group
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3),
                                              heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

        // Group: horizontal stack of 3 items, full width, one-third height of its section
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                               heightDimension: .fractionalWidth(1/3)) // Make group height proportional to width
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item, item])
        // Or, more dynamically: NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, count: 3)

        // Section: contains the group
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        
        return UICollectionViewCompositionalLayout(section: section)
    }

    private func configureDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in

            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GridCell.reuseIdentifier, for: indexPath) as! GridCell
            cell.label.text = "\(identifier)"
            return cell
        }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
        snapshot.appendSections([.main])
        snapshot.appendItems(Array(0..<90))
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

// Example custom cell for grid
class GridCell: UICollectionViewCell {
    static let reuseIdentifier = "GridCell"
    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func configure() {
        backgroundColor = .systemTeal.withAlphaComponent(0.6)
        layer.cornerRadius = 8
        label.font = UIFont.preferredFont(forTextStyle: .title2)
        label.textAlignment = .center
        label.textColor = .white
        
        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
        ])
    }
}

Key changes for the grid:

  • itemSize: Each item is 1/3 of the group's width and 1.0 (full) of the group's height.
  • groupSize: We use fractionalWidth(1.0) for the group's width and fractionalWidth(1/3) for its height. This makes the group's height always 1/3 of its width, ensuring square cells if the items within the group are also square.
  • NSCollectionLayoutGroup.horizontal(layoutSize:subitems:): This creates a horizontal row of items. We explicitly pass [item, item, item] to ensure 3 items per row. Alternatively, count: 3 can be used.

Working with Multiple Sections and Different Layouts

The true power of CompositionalLayout comes when you need different layouts for different sections within the same collection view. This is achieved by creating an array of NSCollectionLayoutSection instances and passing a section provider closure to the UICollectionViewCompositionalLayout initializer.

Let's create a layout with a horizontal scrolling list in the first section and a grid in the second.

Compositional Layout with Multiple Sections UICollectionView Section 0: Horizontal List Item A Item B Item C ... (Scrollable) Section 1: Grid Layout Cell 1 Cell 2 Cell 3 Cell 4 Cell 5 Cell 6
import UIKit

class MixedLayoutViewController: UIViewController {

    enum Section: Int, CaseIterable {
        case featuredProducts
        case categories
    }

    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, Int>!

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Mixed Layout"
        configureHierarchy()
        configureDataSource()
    }

    private func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createMixedLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        view.addSubview(collectionView)

        // Register cells
        collectionView.register(ListCell.self, forCellWithReuseIdentifier: ListCell.reuseIdentifier)
        collectionView.register(GridCell.self, forCellWithReuseIdentifier: GridCell.reuseIdentifier)
    }

    private func createMixedLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            guard let sectionKind = Section(rawValue: sectionIndex) else { return nil }

            switch sectionKind {
            case .featuredProducts:
                // Horizontal scrolling list
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                      heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), // 90% of screen width
                                                       heightDimension: .absolute(150)) // Fixed height
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

                let section = NSCollectionLayoutSection(group: group)
                section.orthogonalScrollingBehavior = .continuous // Make it horizontally scrollable
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
                section.interGroupSpacing = 10
                return section

            case .categories:
                // Grid layout (3 items per row)
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3),
                                                      heightDimension: .fractionalHeight(1.0))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                       heightDimension: .absolute(100)) // Fixed height for grid row
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item, item])

                let section = NSCollectionLayoutSection(group: group)
                section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
                section.interGroupSpacing = 10
                return section
            }
        }
        return layout
    }

    private func configureDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in

            guard let sectionKind = Section(rawValue: indexPath.section) else {
                fatalError("Unknown section")
            }

            switch sectionKind {
            case .featuredProducts:
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ListCell.reuseIdentifier, for: indexPath) as! ListCell
                cell.label.text = "Featured \(identifier)"
                cell.backgroundColor = .systemRed.withAlphaComponent(0.6)
                return cell
            case .categories:
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GridCell.reuseIdentifier, for: indexPath) as! GridCell
                cell.label.text = "Cat \(identifier)"
                cell.backgroundColor = .systemGreen.withAlphaComponent(0.6)
                return cell
            }
        }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
        snapshot.appendSections([.featuredProducts, .categories])
        snapshot.appendItems(Array(0..<10), toSection: .featuredProducts)
        snapshot.appendItems(Array(100..<120), toSection: .categories)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

// Reusing GridCell from before, and adding a ListCell
class ListCell: UICollectionViewCell {
    static let reuseIdentifier = "ListCell"
    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func configure() {
        backgroundColor = .systemBlue.withAlphaComponent(0.6)
        layer.cornerRadius = 8
        label.font = UIFont.preferredFont(forTextStyle: .headline)
        label.textAlignment = .center
        label.textColor = .white
        
        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            label.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 5),
            label.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -5)
        ])
    }
}

In the createMixedLayout() function:

  • We use the UICollectionViewCompositionalLayout(sectionProvider:) initializer. This closure is called for each section index, allowing you to return a different NSCollectionLayoutSection based on the sectionIndex.
  • For .featuredProducts, we define a horizontal group and set section.orthogonalScrollingBehavior = .continuous to make the section scroll horizontally.
  • For .categories, we define our 3-column grid again.
  • The layoutEnvironment parameter in the section provider closure is crucial for adaptive layouts, allowing you to adjust layout based on the current trait collection, container size, etc.

Advanced Considerations

  • Supplementary Views: You can easily add headers, footers, and other supplementary views to your sections using NSCollectionLayoutBoundarySupplementaryItem. These are defined within the NSCollectionLayoutSection.
  • Decoration Views: For background visuals or custom separators that aren't tied to data, NSCollectionLayoutDecorationItem allows you to add aesthetic elements directly to the layout.
  • Adaptive Layouts: The layoutEnvironment parameter in the sectionProvider closure is key. You can check layoutEnvironment.container.effectiveContentSize or layoutEnvironment.traitCollection to create layouts that respond dynamically to size changes (e.g., device rotation, iPad multitasking).
  • Performance: CompositionalLayout is highly optimized. By defining layouts declaratively, the system can perform efficient calculations. When combined with UICollectionViewDiffableDataSource, you get excellent performance and simplified data management.

Summary

UICollectionViewCompositionalLayout offers a truly modern and flexible approach to building complex and adaptive collection view layouts. By embracing its hierarchical model of items, groups, and sections, you can create intricate designs with significantly less code and greater clarity than traditional methods. Its declarative nature simplifies layout logic, promotes reusability, and integrates seamlessly with DiffableDataSource for powerful and performant UIs. If you're building any non-trivial UICollectionView layout, CompositionalLayout is the way to go.

Happy Swifting!