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:
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
| Property | Type | Description |
|---|---|---|
dataSource | UICollectionViewDataSource? | Provides data (e.g., number of items, cells). |
delegate | UICollectionViewDelegate? | Handles interactions and customization. |
collectionViewLayout | UICollectionViewLayout | Defines the layout of items and views. |
allowsSelection | Bool | Enables/disables item selection. |
allowsMultipleSelection | Bool | Enables multiple item selection. |
backgroundView | UIView? | Background view behind the collection. |
Example: Configuring Properties:
collectionView.allowsSelection = true
collectionView.allowsMultipleSelection = false
collectionView.backgroundColor = .systemBackgroundKey Methods of UICollectionView
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
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:
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
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
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:
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:
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:
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:
func removeItem(at index: Int) {
data.remove(at: index)
let indexPath = IndexPath(item: index, section: 0)
collectionView.deleteItems(at: [indexPath])
}Batch Updates:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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 = layoutExample: List Layout:
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:
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
accessibilityLabelandaccessibilityTraitsfor cells.swiftcell.accessibilityLabel = "Item \(indexPath.item)" cell.accessibilityTraits = .buttonAuto Layout: Use constraints (e.g., SnapKit) in custom cells.
Performance: Optimize layouts and use
prefetchingfor large datasets.Testing: Verify selection, animations, and layouts across devices.
Diffable Data Source: Prefer
UICollectionViewDiffableDataSourcefor modern apps.Layout Choice: Use
CompositionalLayoutfor modern, adaptive designs; fall back toFlowLayoutfor simpler grids.
Troubleshooting
- Cells Not Displaying: Check
dataSourceandcellForItemAtimplementation. - Reuse Issues: Ensure unique reuse identifiers.
- Animations Failing: Use
performBatchUpdatesorapplycorrectly. - 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
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
}
}