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 = .systemBackground
Key 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 = layout
Example: 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
accessibilityLabel
andaccessibilityTraits
for cells.swiftcell.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 toFlowLayout
for simpler grids.
Troubleshooting
- Cells Not Displaying: Check
dataSource
andcellForItemAt
implementation. - Reuse Issues: Ensure unique reuse identifiers.
- Animations Failing: Use
performBatchUpdates
orapply
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
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
}
}