Skip to content

UICollectionView

UICollectionView is a UIKit class that displays a flexible, scrollable arrangement of items, supporting grid-like, list, or custom layouts. As a subclass of UIScrollView, it is highly customizable, using UICollectionViewLayout for visual arrangement and data source/delegate protocols for content and interactions. UICollectionViewCell represents individual items, and custom cells allow tailored designs. This document covers the key properties, methods, and usage of UICollectionView, including sections on SwiftUI integration, item management, selection, diffable data sources, multiple sections, UICollectionViewController, and layouts, along with examples and best practices.

Overview of UICollectionView

UICollectionView organizes data into sections and items, offering dynamic layouts via UICollectionViewLayout. It uses UICollectionViewDataSource for content, UICollectionViewDelegate for interactions, and supports reusable cells and supplementary views (e.g., headers) for performance.

Creating a UICollectionView

Create a UICollectionView programmatically or via Interface Builder, specifying a layout (e.g., UICollectionViewFlowLayout).

Programmatic Example:

swift
import UIKit

let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 100)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
view.addSubview(collectionView)

Key Properties of UICollectionView

PropertyTypeDescription
dataSourceUICollectionViewDataSource?Provides data (e.g., number of items, cells).
delegateUICollectionViewDelegate?Handles interactions and customization.
collectionViewLayoutUICollectionViewLayoutDefines the layout of items and views.
allowsSelectionBoolEnables/disables item selection.
allowsMultipleSelectionBoolEnables multiple item selection.
backgroundViewUIView?Background view behind the collection.

Example: Configuring Properties:

swift
collectionView.allowsSelection = true
collectionView.allowsMultipleSelection = false
collectionView.backgroundColor = .systemBackground

Key Methods of UICollectionView

MethodDescription
register(_:forCellWithReuseIdentifier:)Registers a cell class or nib.
dequeueReusableCell(withReuseIdentifier:for:)Dequeues a reusable cell.
reloadData()Reloads all data.
insertItems(at:)Inserts items with animation.
deleteItems(at:)Deletes items with animation.
selectItem(at:animated:scrollPosition:)Selects an item programmatically.

UICollectionViewDataSource Protocol

MethodDescription
collectionView(_:numberOfItemsInSection:)Returns the number of items in a section.
collectionView(_:cellForItemAt:)Returns a configured cell for an item.
numberOfSections(in:)Returns the number of sections (optional).

UICollectionViewDelegate Protocol

MethodDescription
collectionView(_:didSelectItemAt:)Handles item selection.
collectionView(_:shouldSelectItemAt:)Determines if an item can be selected.
collectionView(_:layout:sizeForItemAt:)Returns custom item size (with flow layout).

Creating Custom UICollectionViewCells

Custom cells allow unique layouts, created programmatically or via nibs/storyboards.

Programmatic Custom Cell

swift
class CustomCollectionViewCell: UICollectionViewCell {
    static let identifier = "CustomCell"
    
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        label.textAlignment = .center
        label.font = .systemFont(ofSize: 16, weight: .medium)
        contentView.addSubview(label)
        contentView.backgroundColor = .systemGray6
        contentView.layer.cornerRadius = 8
        
        label.snp.makeConstraints { make in
            make.edges.equalTo(contentView).inset(8)
        }
    }
    
    func configure(with text: String) {
        label.text = text
    }
}

Register and Use:

swift
collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
    cell.configure(with: "Item \(indexPath.item)")
    return cell
}

Using SwiftUI to Create a UICollectionViewCell

SwiftUI views can be hosted in UICollectionViewCell using UIHostingController for hybrid apps.

SwiftUI Cell View

swift
import SwiftUI

struct CustomCellView: View {
    let text: String
    
    var body: some View {
        Text(text)
            .font(.system(size: 16, weight: .medium))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color(UIColor.systemGray6))
            .cornerRadius(8)
            .padding(8)
    }
}

Hosting in UICollectionViewCell

swift
class SwiftUICustomCell: UICollectionViewCell {
    static let identifier = "SwiftUICell"
    private var hostingController: UIHostingController<CustomCellView>?
    
    func configure(with text: String) {
        if hostingController == nil {
            let contentView = CustomCellView(text: text)
            hostingController = UIHostingController(rootView: contentView)
            hostingController!.view.translatesAutoresizingMaskIntoConstraints = false
            self.contentView.addSubview(hostingController!.view)
            
            hostingController!.view.snp.makeConstraints { make in
                make.edges.equalTo(contentView)
            }
        } else {
            hostingController!.rootView = CustomCellView(text: text)
        }
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        hostingController?.view.removeFromSuperview()
        hostingController = nil
    }
}

Register and Use:

swift
collectionView.register(SwiftUICustomCell.self, forCellWithReuseIdentifier: SwiftUICustomCell.identifier)

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SwiftUICustomCell.identifier, for: indexPath) as! SwiftUICustomCell
    cell.configure(with: "SwiftUI Item \(indexPath.item)")
    return cell
}

Inserting Items Into a UICollectionView

Insert items with animation using insertItems(at:).

Example:

swift
var data = ["Item 1", "Item 2"]

func addItem() {
    data.append("Item \(data.count + 1)")
    let indexPath = IndexPath(item: data.count - 1, section: 0)
    collectionView.insertItems(at: [indexPath])
}

Batch Updates:

swift
collectionView.performBatchUpdates {
    data.insert("New Item", at: 0)
    collectionView.insertItems(at: [IndexPath(item: 0, section: 0)])
}

Deleting Items from a Collection View

Delete items with animation using deleteItems(at:).

Example:

swift
func removeItem(at index: Int) {
    data.remove(at: index)
    let indexPath = IndexPath(item: index, section: 0)
    collectionView.deleteItems(at: [indexPath])
}

Batch Updates:

swift
collectionView.performBatchUpdates {
    data.remove(at: 1)
    collectionView.deleteItems(at: [IndexPath(item: 1, section: 0)])
}

Supporting Item Selection in a UICollectionView

Handle selection via collectionView(_:didSelectItemAt:) and programmatically select items with selectItem.

Example:

swift
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    print("Selected: \(data[indexPath.item])")
    collectionView.deselectItem(at: indexPath, animated: true)
}

func selectItemProgrammatically(at index: Int) {
    let indexPath = IndexPath(item: index, section: 0)
    collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
}

Enable Multiple Selection:

swift
collectionView.allowsMultipleSelection = true

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    if let selectedItems = collectionView.indexPathsForSelectedItems {
        print("Selected items: \(selectedItems.map { data[$0.item] })")
    }
}

UICollectionViewDiffableDataSource

Introduced in iOS 13, UICollectionViewDiffableDataSource simplifies data management with type-safe, animated updates.

Example:

swift
enum Section { case main }
typealias DataSource = UICollectionViewDiffableDataSource<Section, String>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, String>

var dataSource: DataSource!

private func configureDataSource() {
    dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, item in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
        cell.configure(with: item)
        return cell
    }
}

private func applySnapshot(data: [String], animatingDifferences: Bool = true) {
    var snapshot = Snapshot()
    snapshot.appendSections([.main])
    snapshot.appendItems(data)
    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

Supporting Multiple Sections in UICollectionView with Diffable Datasource

Diffable data sources handle multiple sections with unique identifiers.

Example:

swift
enum Section: Hashable {
    case featured
    case regular
}

struct Item: Hashable {
    let id = UUID()
    let title: String
}

typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>

var dataSource: DataSource!
var featuredItems: [Item] = [Item(title: "Featured 1"), Item(title: "Featured 2")]
var regularItems: [Item] = [Item(title: "Regular 1"), Item(title: "Regular 2")]

private func configureDataSource() {
    dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, item in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
        cell.configure(with: item.title)
        return cell
    }
    
    dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
        header.configure(with: indexPath.section == 0 ? "Featured" : "Regular")
        return header
    }
}

private func applySnapshot(animatingDifferences: Bool = true) {
    var snapshot = Snapshot()
    snapshot.appendSections([.featured, .regular])
    snapshot.appendItems(featuredItems, toSection: .featured)
    snapshot.appendItems(regularItems, toSection: .regular)
    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

Header View:

swift
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
    }
}

Supporting Multiple Sections in UICollectionView the Old Way

Before iOS 13, multiple sections were managed manually with UICollectionViewDataSource.

Example:

swift
var featuredItems = ["Featured 1", "Featured 2"]
var regularItems = ["Regular 1", "Regular 2"]

func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 2
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return section == 0 ? featuredItems.count : regularItems.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
    let item = indexPath.section == 0 ? featuredItems[indexPath.item] : regularItems[indexPath.item]
    cell.configure(with: item)
    return cell
}

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
    header.configure(with: indexPath.section == 0 ? "Featured" : "Regular")
    return header
}

UICollectionViewController

UICollectionViewController is a UIViewController subclass that manages a UICollectionView, automatically setting itself as the collection’s data source and delegate. It simplifies setup but is less flexible for complex layouts.

Example:

swift
class CollectionViewController: UICollectionViewController {
    var data = ["Item 1", "Item 2", "Item 3"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.backgroundColor = .systemBackground
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
        cell.configure(with: data[indexPath.item])
        return cell
    }
}
  • Pros: Simplified setup, built-in collection view management.
  • Cons: Less flexible for multiple views or custom layouts.
  • Use Case: Simple collection-based apps.

All Different Layouts in UICollectionView

UICollectionView supports various layouts via UICollectionViewLayout subclasses, with UICollectionViewFlowLayout and UICollectionViewCompositionalLayout (iOS 13+) being the most common.

1. UICollectionViewFlowLayout

A grid-based layout with customizable item sizes, spacing, and scroll direction.

Example:

swift
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: 100, height: 100)
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
collectionView.collectionViewLayout = layout
  • Use Case: Simple grids or lists (e.g., photo galleries).

2. UICollectionViewCompositionalLayout (iOS 13+)

A modern, flexible layout for complex arrangements using sections, groups, and items.

Example: Grid Layout:

swift
let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
    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)
    
    return section
}
collectionView.collectionViewLayout = layout

Example: List Layout:

swift
let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .plain))
collectionView.collectionViewLayout = layout
  • Use Case: Complex layouts, adaptive designs, or list-like appearances.

3. Custom UICollectionViewLayout

Create a subclass of UICollectionViewLayout for fully custom layouts (e.g., circular or spiral arrangements).

Example: Basic Custom Layout:

swift
class CustomLayout: UICollectionViewLayout {
    override func prepare() {
        // Calculate layout attributes
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // Return attributes for visible items
        return []
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        // Return attributes for specific item
        return nil
    }
}
  • Use Case: Unique, non-standard layouts (e.g., circular photo carousel).

Best Practices

  • Reuse Cells: Register and dequeue cells/supplementary views for performance.

  • Accessibility: Set accessibilityLabel and accessibilityTraits for cells.

    swift
    cell.accessibilityLabel = "Item \(indexPath.item)"
    cell.accessibilityTraits = .button
  • Auto Layout: Use constraints (e.g., SnapKit) in custom cells.

  • Performance: Optimize layouts and use prefetching for large datasets.

  • Testing: Verify selection, animations, and layouts across devices.

  • Diffable Data Source: Prefer UICollectionViewDiffableDataSource for modern apps.

  • Layout Choice: Use CompositionalLayout for modern, adaptive designs; fall back to FlowLayout for simpler grids.

Troubleshooting

  • Cells Not Displaying: Check dataSource and cellForItemAt implementation.
  • Reuse Issues: Ensure unique reuse identifiers.
  • Animations Failing: Use performBatchUpdates or apply correctly.
  • Layout Issues: Verify layout properties (e.g., item sizes, insets).
  • Accessibility Issues: Test with VoiceOver.
  • Performance Issues: Profile with Instruments for heavy layouts.

Example: Complete UICollectionView Setup with Diffable Data Source

swift
import UIKit
import SnapKit

class CustomCollectionViewCell: UICollectionViewCell {
    static let identifier = "CustomCell"
    
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        label.textAlignment = .center
        label.font = .systemFont(ofSize: 16, weight: .medium)
        contentView.addSubview(label)
        contentView.backgroundColor = .systemGray6
        contentView.layer.cornerRadius = 8
        
        label.snp.makeConstraints { make in
            make.edges.equalTo(contentView).inset(8)
        }
    }
    
    func configure(with text: String) {
        label.text = text
        accessibilityLabel = text
    }
}

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
    }
}

enum Section: Hashable {
    case featured
    case regular
}

struct Item: Hashable {
    let id = UUID()
    let text: String
}

typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>

class ViewController: UIViewController, UICollectionViewDelegate {
    private let collectionView: UICollectionView
    private var dataSource: DataSource!
    private var featuredItems = [Item(text: "Featured 1"), Item(text: "Featured 2")]
    private var regularItems = [Item(text: "Regular 1"), Item(text: "Regular 2")]
    
    init() {
        let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
            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
        }
        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()
        configureCollectionView()
        configureDataSource()
        applySnapshot()
    }
    
    private func configureCollectionView() {
        collectionView.register(CustomCollectionViewCell.self, forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.identifier)
        collectionView.delegate = self
        view.addSubview(collectionView)
        
        collectionView.snp.makeConstraints { make in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
    }
    
    private func configureDataSource() {
        dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, item in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier, for: indexPath) as! CustomCollectionViewCell
            cell.configure(with: item.text)
            return cell
        }
        
        dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
            let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.identifier, for: indexPath) as! HeaderView
            header.configure(with: indexPath.section == 0 ? "Featured" : "Regular")
            return header
        }
    }
    
    private func applySnapshot(animatingDifferences: Bool = true) {
        var snapshot = Snapshot()
        snapshot.appendSections([.featured, .regular])
        snapshot.appendItems(featuredItems, toSection: .featured)
        snapshot.appendItems(regularItems, toSection: .regular)
        dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let item = indexPath.section == 0 ? featuredItems[indexPath.item] : regularItems[indexPath.item]
        print("Selected: \(item.text)")
        collectionView.deselectItem(at: indexPath, animated: true)
    }
}

// App setup
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let navController = UINavigationController(rootViewController: ViewController())
        window?.rootViewController = navController
        window?.makeKeyAndVisible()
        return true
    }
}

Resources

Released under the MIT License.