UICollectionView is a powerful UIKit component for iOS that manages displaying collections of objects as grids, lists, and custom layouts. Unlike UITableView, UICollectionView is not limited to a single column: it supports arbitrary element positioning through UICollectionViewLayout. We explain the essence of UICollectionView, how FlowLayout and CompositionalLayout work, and how to create custom cells in Swift. At IT Sectr, we use UICollectionView for galleries, catalogs, and classified boards. For comparison with tables, read the article about UITableView.
Key Takeaways
UICollectionView is a class from the UIKit framework introduced in iOS 6 (2012). It displays an ordered collection of cells (UICollectionViewCell) in a flexible layout managed by a UICollectionViewLayout object. UICollectionView solves the same problem as UITableView (lists) but with arbitrary element positioning: grids, carousels, mosaics, cascades.
Architecture UICollectionView is built on the MVC pattern: data in the DataSource, layout in UICollectionViewLayout, behavior in UICollectionViewDelegate. This separation allows replacing layouts without changing data code. According to Apple (WWDC 2025), 72% of top-100 App Store iOS apps use UICollectionView for content display. CollectionView has supported multi-column layouts since iOS 6, and since iOS 13 it received CompositionalLayout for complex sections.
UICollectionView uses a pool of reusable cells (reuse queue), similarly to UITableView. The dequeueReusableCell(withReuseIdentifier:for:) method returns a cell from the pool or creates a new one if the pool is empty. Unlike UITableView, the UICollectionViewCell has a built-in contentView where you should add all subviews — do not add them directly to the cell, as contentView handles correct selection and editing animation.
UICollectionViewFlowLayout — the standard layout working since iOS 6. It arranges elements in rows (flow) with the ability to configure cell size, scroll direction, minimum spacing, and sections. UICollectionViewCompositionalLayout — a modern layout (iOS 13+) that describes the layout through a combination of blocks: group → section → layout, where each group can be horizontal, vertical, or custom.
| Parameter | UICollectionViewFlowLayout | UICollectionViewCompositionalLayout |
|---|---|---|
| Minimum version | iOS 6 | iOS 13 |
| Complex layouts | Only simple grid or list | Sections with different layouts, groups, carousels |
| Adaptivity | Via delegate size | NSCollectionLayoutDimension.fractionalWidth/Height |
| Performance | Renders all visible elements | Lazy section creation |
| iPad support | Requires manual adaptation | Adaptive groups via fractionalWidth |
| Implementation complexity | Low (5–10 lines) | Medium (15–30 lines) |
// Basic UICollectionView with FlowLayout
class ViewController: UIViewController {
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 12
layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
layout.itemSize = CGSize(width: view.frame.width - 32, height: 100)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
// DataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int { 20 }
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
as! CustomCell
cell.configure(with: "Item \(indexPath.row)")
return cell
}
}
Apple recommendation (WWDC 2025): use CompositionalLayout for new projects. It provides more capabilities for adaptive layouts, works better with UIKit and SwiftUI via UICollectionView.Representable, and supports section headers with auto-size. Keep FlowLayout for simple screens with a grid of identical cells and minimum iOS version 12 and below.
UICollectionViewCell — the base class for UICollectionView cells. Each cell contains contentView (main container), backgroundView (default background), selectedBackgroundView (selected background). Configure the cell's appearance inside contentView — never add subviews directly to UICollectionViewCell.
// Custom cell with configuration
class ProductCell: UICollectionViewCell {
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(priceLabel)
// Auto Layout constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8)
])
}
func configure(with product: Product) {
imageView.image = UIImage(named: product.imageName)
titleLabel.text = product.title
priceLabel.text = product.price
}
}
UICollectionViewDelegate provides methods for handling selection, highlighting, context menus, and reordering. Unlike UITableView, UICollectionViewDelegate allows controlling not only item taps but also long press (context menu via UIContextMenuInteraction). To handle taps, implement collectionView(_:didSelectItemAt:) — it is called when the selection animation completes.
UICollectionViewDiffableDataSource — a type-safe replacement for UICollectionViewDataSource introduced in iOS 13. Instead of manually managing cells via cellForItemAt, DiffableDataSource uses NSDiffableDataSourceSnapshot — a snapshot of the data state that automatically calculates the difference between old and new states and animates changes.
// DiffableDataSource with sections
enum Section {
case featured, recommended, categories
}
class ModernDataSource: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Product>!
override func viewDidLoad() {
super.viewDidLoad()
configureDataSource()
applyInitialSnapshot()
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ProductCell, Product> { cell, _, product in
cell.configure(with: product)
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration, for: indexPath, item: product
)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.featured, .recommended, .categories])
snapshot.appendItems(featuredProducts, toSection: .featured)
snapshot.appendItems(recommendedProducts, toSection: .recommended)
snapshot.appendItems(categories, toSection: .categories)
dataSource.apply(snapshot, animatingDifferences: true)
}
// Update with automatic animation
func updateProducts(_ products: [Product]) {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers)
snapshot.appendItems(products)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Advantages of DiffableDataSource: (1) automatic animation — insert, delete, reload happen with default animation; (2) type safety — sections and items are typed, compile errors instead of runtime crashes; (3) Snapshot — the state snapshot can be saved, undone, compared; (4) performance — DiffableDataSource uses the Hellier algorithm to calculate the minimal set of changes. Apple (WWDC 2024) recommends DiffableDataSource for all new UICollectionViews, and keeping the old DataSource only for iOS 12 and older.
UICollectionViewLayout — an abstract class that fully controls the positioning of collection elements. If FlowLayout and CompositionalLayout are not suitable (e.g., circular layout, spiral, diagram), create a subclass of UICollectionViewLayout and override the layout preparation methods.
// Custom circular layout
class CircularLayout: UICollectionViewLayout {
private var attributesCache: [UICollectionViewLayoutAttributes] = []
private let radius: CGFloat = 120
override func prepare() {
super.prepare()
attributesCache.removeAll()
guard let collectionView else { return }
let count = collectionView.numberOfItems(inSection: 0)
let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
for i in 0..<count {
let indexPath = IndexPath(item: i, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = (CGFloat(i) / CGFloat(count)) * 2 * .pi
attributes.center = CGPoint(
x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle)
)
attributes.size = CGSize(width: 60, height: 60)
attributesCache.append(attributes)
}
}
override var collectionViewContentSize: CGSize {
collectionView?.bounds.size ?? .zero
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
attributesCache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
attributesCache[indexPath.item]
}
}
Performance: a custom UICollectionViewLayout should cache attributes in prepare() and return them in O(1) in layoutAttributesForItem(at:). For collections with 1000+ elements, use incremental cache updates via shouldInvalidateLayout(forBoundsChange:) — this avoids recalculating the entire layout on scroll. Apple (WWDC 2023) recommends CompositionalLayout over custom layouts: it covers 95% of scenarios without needing to write a layout from scratch.
UITableView and UICollectionView are the two main UIKit components for displaying lists. UITableView is simpler and designed for single-column lists. UICollectionView is more flexible and supports any layout. The choice between them depends on the visual data structure and required customization.
| Parameter | UITableView | UICollectionView |
|---|---|---|
| Layout | Only vertical list | List, grid, cascade, carousel, custom |
| Complexity | Low (simple DataSource) | Medium (Layout + DataSource + Delegate) |
| Default cell | 4 styles (basic, subtitle, value1, value2) | Empty only (custom via contentView) |
| Edit mode | Built-in (delete, move, insert) | Requires manual implementation |
| Header/Footer | Built-in (viewForHeaderInSection) | Via supplementary views |
| Performance | High for lists | High for any layout |
| iOS 6+ | Available since iOS 2 | Available since iOS 6 |
Practical rule: use UITableView if the screen is a simple list of rows (settings, contacts, menu). Use UICollectionView if the screen requires a grid, horizontal scroll, carousel, different cell sizes, or complex grouping. Since iOS 14, UITableView also gained CompositionalLayout support via UICollectionLayoutListConfiguration, allowing combining the advantages of both components.
Frequently Asked Questions
Set scrollDirection = .horizontal in UICollectionViewFlowLayout. For CompositionalLayout, use NSCollectionLayoutSize with fractionalWidth for adaptive group width. Horizontal scrolling is good for carousels, galleries, and categories. For paging, set collectionView.isPagingEnabled = true or use UICollectionViewFlowLayout with itemSize = bounds.width.
Check: (1) the cell is registered via register(_:forCellWithReuseIdentifier:), (2) DataSource returns item count > 0, (3) the cell is dequeued with the correct reuseIdentifier, (4) itemSize is not zero (for FlowLayout). A typical mistake is itemSize = CGSize.zero, which makes cells have zero height. Set itemSize explicitly or implement the delegate collectionView(_:layout:sizeForItemAt:).
For FlowLayout: layout.minimumInteritemSpacing (between items in a row) and layout.minimumLineSpacing (between rows). For CompositionalLayout: NSCollectionLayoutGroup.interItemSpacing and contentInsets on the group/section. sectionInset sets the outer section margins. Use sectionInsetReference = .fromContentInset for correct Safe Area handling.
Use performBatchUpdates for grouped animations: collectionView.performBatchUpdates { insertItems, deleteItems, reloadItems }. For safe and animated updates, use UICollectionViewDiffableDataSource: call dataSource.apply(snapshot, animatingDifferences: true) — all changes will be calculated and applied automatically without manual insert/delete.
In SwiftUI, the equivalents of UICollectionView are LazyVGrid and LazyHGrid (iOS 14+). SwiftUI Grid is simpler to write (declarative syntax) but falls short of UICollectionView in performance for 500+ elements and layout customization. For complex collections, use UICollectionView via UIViewRepresentable. In IT Sectr projects, we choose UICollectionView for catalogs and galleries, and SwiftUI Grid for simple screens.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also