How to Create iOS Grid Layouts with UICollectionView and Diffable Data Source

Create grid layouts in iOS using UICollectionView with UICollectionViewFlowLayout and UICollectionViewDiffableDataSource. UICollectionView displays multi-column grids of data items. UICollectionViewFlowLayout organizes those items into a configurable grid with spacing, sizing, and scrolling controls. UICollectionViewDiffableDataSource manages data through snapshots instead of traditional data source methods.

What Is UICollectionView and How Do Grid Layouts Work?

UICollectionView is a UIKit class that presents ordered collections of data in customizable arrangements. Apple introduced it in iOS 6 as a more flexible alternative to UITableView. While table views are limited to single-column vertical lists, collection views support grids, stacks, circular layouts, and any arrangement you define.

A UICollectionView has three component types. Cells (UICollectionViewCell) display individual data items. Supplementary views provide headers and footers for sections. Decoration views add visual backgrounds that are not tied to data.

How to Configure UICollectionViewFlowLayout for Grid Layouts

UICollectionViewFlowLayout is the built-in layout class that arranges items in a grid. Set the itemSize to define each cell's dimensions. Set minimumInteritemSpacing for horizontal spacing between items. Set minimumLineSpacing for vertical spacing between rows.

let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 150)
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
layout.scrollDirection = .vertical
collectionView.collectionViewLayout = layout

Set estimatedItemSize to .zero for fixed-size cells. When estimatedItemSize is not zero, the system dynamically calculates sizes based on content, which can produce inconsistent grid layouts. Set scrollDirection to .horizontal for horizontal scrolling grids.

How to Use UICollectionViewDiffableDataSource for Data Management

UICollectionViewDiffableDataSource replaces the traditional UICollectionViewDataSource protocol. It manages data through snapshots and automatically animates changes between snapshots. Create it with a cell provider closure.

enum Section: Hashable {
    case main
}

struct Item: Hashable {
    let id: UUID
    let name: String
    let price: Double
}

var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

dataSource = UICollectionViewDiffableDataSource<Section, Item>(
    collectionView: collectionView
) { collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "Cell",
        for: indexPath
    ) as! IconCollectionViewCell
    cell.nameLabel.text = item.name
    cell.priceLabel.text = "$(item.price)"
    return cell
}

The generic parameters define the section type and item type. Both must conform to Hashable. The cell provider closure runs for each visible item and returns a configured cell.

How to Apply Snapshots with NSDiffableDataSourceSnapshot

NSDiffableDataSourceSnapshot captures the state of your data at a point in time. Create a snapshot, append sections and items, and apply it to the data source.

var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)

The apply method computes the difference between the current data and the new snapshot. It automatically inserts, deletes, and rearranges cells with built-in animations. Set animatingDifferences to true for animated updates. Set it to false for initial data loads.

How to Create and Register Custom UICollectionViewCell Subclasses

Create a UICollectionViewCell subclass with outlets for your data. Register it with the collection view using either a nib file or a class reference.

class ProductCell: UICollectionViewCell {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var priceLabel: UILabel!
}

collectionView.register(
    ProductCell.self,
    forCellWithReuseIdentifier: "ProductCell"
)

For storyboard-based cells, set the reuse identifier in the Attributes inspector. The cell provider dequeue method uses this identifier to create or recycle cells.

How to Handle Multiple Sections with Diffable Data Source

Define an enum with multiple cases for sections with different data types. Each case becomes a separate section in the snapshot.

enum Section: Int, Hashable, CaseIterable {
    case featured
    case popular
    case all
}

Append each section independently and add items to specific sections.

var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases)
snapshot.appendItems(featuredItems, toSection: .featured)
snapshot.appendItems(popularItems, toSection: .popular)
snapshot.appendItems(allItems, toSection: .all)
dataSource.apply(snapshot, animatingDifferences: false)

Each section can have its own header, footer, and layout configuration. The snapshot preserves the order of sections and items.

How to Add Section Headers and Footers

Register a UICollectionReusableView subclass for supplementary views. Configure the data source to provide headers.

collectionView.register(
    SectionHeaderView.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: SectionHeaderView.reuseIdentifier
)

dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
    guard kind == UICollectionView.elementKindSectionHeader else { return nil }
    let header = collectionView.dequeueReusableSupplementaryView(
        ofKind: kind,
        withReuseIdentifier: SectionHeaderView.reuseIdentifier,
        for: indexPath
    ) as! SectionHeaderView
    header.titleLabel.text = Section(rawValue: indexPath.section)?.title
    return header
}

Set the headerReferenceSize and footerReferenceSize on the flow layout to define header dimensions.

layout.headerReferenceSize = CGSize(width: 0, height: 50)
layout.footerReferenceSize = CGSize(width: 0, height: 30)

Practical Tips for Grid Layouts in iOS

Conform model types to Hashable for Diffable Data Source. The compiler requires Hashable conformance because the data source uses hash values to compute differences between snapshots.

Set estimatedItemSize to .zero for fixed grid dimensions. The default Automatic estimate size mode causes the flow layout to calculate dynamic sizes, which breaks consistent grid spacing.

Use lazy initialization for data source properties. The lazy keyword ensures the data source initializes after self is available in viewDidLoad.

Register cells in viewDidLoad before applying the first snapshot. The collection view must have a registered cell class or nib for the reuse identifier used in the cell provider.

Disable automaticallyAdjustsScrollViewInsets when using safe area layouts in older iOS versions. This prevents double insets in navigation controller scenarios.

Use contentInsets on the flow layout to add padding around the entire grid. Combine this with minimumInteritemSpacing for fine-grained control over cell spacing.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

What iOS version is required? The diffable data source used here requires iOS 13 or later.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

Can I use this with SwiftUI instead of UIKit? The article demonstrates the UIKit approach. SwiftUI has its own APIs for similar functionality. You can adapt the concepts, but the implementation details differ between the two frameworks.

All articles