Skip to main content

Swift Collection Views

Introduction

Collection views are a fundamental UI component in iOS development that allow you to present data in flexible, customizable layouts. Unlike table views which are limited to a single column, collection views can arrange items in grids, horizontal lists, or completely custom layouts. This makes them perfect for image galleries, calendars, complex menus, and many other UI patterns common in modern apps.

In this tutorial, we'll explore UICollectionView in Swift, covering:

  • The basic architecture of collection views
  • Setting up a simple grid layout
  • Customizing cells and layouts
  • Handling user interactions
  • Advanced collection view features

Collection View Basics

At its core, a UICollectionView is composed of several key components:

  1. Collection View: The main view that manages and displays the content
  2. Cells: Reusable views that display individual items
  3. Supplementary Views: Headers, footers, and other decorative elements
  4. Layout: An object that determines how items are positioned (typically UICollectionViewFlowLayout)
  5. Data Source and Delegate: Protocols that provide data and handle interactions

Setting Up Your First Collection View

Let's start by creating a basic collection view with a grid layout:

swift
import UIKit

class BasicCollectionViewController: UIViewController {

private var collectionView: UICollectionView!
private let reuseIdentifier = "Cell"
private let items = Array(1...50) // Sample data

override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}

private func setupCollectionView() {
// Create a flow layout
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 100, height: 100)
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

// Initialize collection view with the layout
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .white

// Register cell class
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)

// Set data source and delegate
collectionView.dataSource = self
collectionView.delegate = self

view.addSubview(collectionView)
}
}

// MARK: - UICollectionViewDataSource
extension BasicCollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)

// Configure the cell's appearance
cell.backgroundColor = .systemBlue
cell.layer.cornerRadius = 8

return cell
}
}

// MARK: - UICollectionViewDelegate
extension BasicCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item \(items[indexPath.item])")
}
}

This example creates a basic grid layout with blue cells. When you run this code, you'll see a collection view with 50 blue square cells arranged in a grid, with appropriate spacing between them.

Creating Custom Collection View Cells

In real applications, you'll want to create custom cells with more complex layouts. Let's create a custom cell:

swift
class PhotoCell: UICollectionViewCell {

static let reuseIdentifier = "PhotoCell"

let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()

let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 12)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews() {
backgroundColor = .white
layer.cornerRadius = 8
layer.masksToBounds = true

// Add subviews
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)

// Setup constraints
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.8),

titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}

func configure(with imageName: String, title: String) {
imageView.image = UIImage(named: imageName)
titleLabel.text = title
}
}

Now we can update our view controller to use this custom cell:

swift
// Define a model for our data
struct Photo {
let imageName: String
let title: String
}

class PhotoGalleryViewController: UIViewController {

private var collectionView: UICollectionView!
private let photos = [
Photo(imageName: "photo1", title: "Sunset"),
Photo(imageName: "photo2", title: "Mountains"),
Photo(imageName: "photo3", title: "Beach"),
// Add more photos as needed
]

override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}

private func setupCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: (view.bounds.width - 30) / 2, height: 180)
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground

// Register our custom cell
collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: PhotoCell.reuseIdentifier)

collectionView.dataSource = self
collectionView.delegate = self

view.addSubview(collectionView)
}
}

// MARK: - UICollectionViewDataSource
extension PhotoGalleryViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photos.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: PhotoCell.reuseIdentifier,
for: indexPath
) as? PhotoCell else {
fatalError("Failed to dequeue PhotoCell")
}

let photo = photos[indexPath.item]
cell.configure(with: photo.imageName, title: photo.title)

return cell
}
}

// MARK: - UICollectionViewDelegate
extension PhotoGalleryViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let photo = photos[indexPath.item]
print("Selected photo: \(photo.title)")
}
}

Working with Collection View Layouts

Flow Layout

The UICollectionViewFlowLayout is the most common layout used with collection views. It arranges items in a line, wrapping to the next line when needed. We've already seen it used in previous examples.

Here's how to customize a flow layout for different scenarios:

swift
// For a grid layout (default flow)
let gridLayout = UICollectionViewFlowLayout()
gridLayout.itemSize = CGSize(width: 100, height: 100)
gridLayout.minimumInteritemSpacing = 10
gridLayout.minimumLineSpacing = 15
gridLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

// For a horizontal scrolling layout
let horizontalLayout = UICollectionViewFlowLayout()
horizontalLayout.scrollDirection = .horizontal
horizontalLayout.itemSize = CGSize(width: 200, height: view.bounds.height - 40)
horizontalLayout.minimumLineSpacing = 20
horizontalLayout.sectionInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)

Compositional Layout (iOS 13+)

For more complex layouts, iOS 13 introduced UICollectionViewCompositionalLayout, which allows you to compose layouts from nested groups:

swift
func createCompositionalLayout() -> UICollectionViewLayout {
// Item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

// Group
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(200))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
subitem: item,
count: 2)

// Section
let section = NSCollectionLayoutSection(group: group)

// Layout
return UICollectionViewCompositionalLayout(section: section)
}

// Usage
let layout = createCompositionalLayout()
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)

This creates a grid with two columns, where each item takes up the full height and width of its allocated space.

Handling User Interaction

Collection views support various user interactions:

Selection

We've already seen the basic selection handler:

swift
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Handle selection
print("Selected item at \(indexPath)")
}

You can also configure multiple selection:

swift
collectionView.allowsMultipleSelection = true

Custom Actions with Long Press Gesture

You can add gesture recognizers to enable additional interactions:

swift
func setupLongPressGesture() {
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressGesture.minimumPressDuration = 0.5
collectionView.addGestureRecognizer(longPressGesture)
}

@objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
let point = gesture.location(in: collectionView)
if let indexPath = collectionView.indexPathForItem(at: point) {
// Show action sheet or context menu
showOptionsForItem(at: indexPath)
}
}
}

func showOptionsForItem(at indexPath: IndexPath) {
let photo = photos[indexPath.item]

let alertController = UIAlertController(
title: photo.title,
message: "Choose an action",
preferredStyle: .actionSheet
)

alertController.addAction(UIAlertAction(title: "Share", style: .default) { _ in
// Share the photo
})

alertController.addAction(UIAlertAction(title: "Delete", style: .destructive) { _ in
// Remove the photo
self.photos.remove(at: indexPath.item)
self.collectionView.deleteItems(at: [indexPath])
})

alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel))

present(alertController, animated: true)
}

Real-World Example: Instagram-like Photo Feed

Let's create a more complex example that resembles an Instagram-like photo feed:

swift
// Photo model
struct FeedPhoto {
let id: String
let username: String
let userAvatar: String
let imageURL: String
let caption: String
let likes: Int
}

class InstagramFeedCell: UICollectionViewCell {
static let reuseIdentifier = "InstagramFeedCell"

// UI components (header with avatar and username, image, like count, caption)
let headerView = UIView()
let avatarImageView = UIImageView()
let usernameLabel = UILabel()
let photoImageView = UIImageView()
let likeButton = UIButton()
let likeCountLabel = UILabel()
let captionLabel = UILabel()

override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews() {
// Setup header
headerView.translatesAutoresizingMaskIntoConstraints = false
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
photoImageView.translatesAutoresizingMaskIntoConstraints = false
likeButton.translatesAutoresizingMaskIntoConstraints = false
likeCountLabel.translatesAutoresizingMaskIntoConstraints = false
captionLabel.translatesAutoresizingMaskIntoConstraints = false

contentView.addSubview(headerView)
headerView.addSubview(avatarImageView)
headerView.addSubview(usernameLabel)
contentView.addSubview(photoImageView)
contentView.addSubview(likeButton)
contentView.addSubview(likeCountLabel)
contentView.addSubview(captionLabel)

// Configure appearance
avatarImageView.layer.cornerRadius = 15
avatarImageView.clipsToBounds = true

usernameLabel.font = UIFont.boldSystemFont(ofSize: 14)

photoImageView.contentMode = .scaleAspectFill
photoImageView.clipsToBounds = true

likeButton.setImage(UIImage(systemName: "heart"), for: .normal)
likeButton.tintColor = .red

likeCountLabel.font = UIFont.systemFont(ofSize: 12, weight: .medium)

captionLabel.font = UIFont.systemFont(ofSize: 13)
captionLabel.numberOfLines = 2

// Set up constraints (simplified for brevity)
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: contentView.topAnchor),
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
headerView.heightAnchor.constraint(equalToConstant: 40),

// Complete the constraints for all UI elements
// ...
])
}

func configure(with photo: FeedPhoto) {
usernameLabel.text = photo.username
// In a real app, you would load images asynchronously
avatarImageView.image = UIImage(named: photo.userAvatar)
photoImageView.image = UIImage(named: photo.imageURL)
likeCountLabel.text = "\(photo.likes) likes"
captionLabel.text = photo.caption
}
}

The implementation of the view controller would follow similar patterns as our previous examples but would use the more complex InstagramFeedCell.

Collection View Performance Tips

When working with collection views, especially ones with many items, performance optimization becomes crucial:

  1. Cell Reuse: Always dequeue reusable cells instead of creating new ones
  2. Asynchronous Image Loading: Load and process images on background threads
  3. Cell Prefetching: Implement UICollectionViewDataSourcePrefetching to prepare cells before they're displayed
  4. Proper Cell Sizing: Calculate and cache cell sizes to avoid redundant layout calculations
  5. Avoid Heavy Work in Cell Configuration: Keep cell configuration lightweight

Here's how to implement prefetching:

swift
// Add protocol conformance
class PhotoFeedViewController: UIViewController, UICollectionViewDataSourcePrefetching {

// Setup in viewDidLoad
collectionView.prefetchDataSource = self

// Implement prefetching method
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let photo = photos[indexPath.item]
// Start loading the image for this photo
ImageLoader.shared.prefetchImage(at: photo.imageURL)
}
}

// Optionally implement cancellation
func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let photo = photos[indexPath.item]
ImageLoader.shared.cancelPrefetching(for: photo.imageURL)
}
}
}

Summary

In this tutorial, we've explored Swift Collection Views in detail, covering:

  • Basic collection view setup and configuration
  • Creating custom collection view cells
  • Working with different layout types
  • Handling user interactions
  • Building a real-world example
  • Performance optimization tips

Collection views are incredibly versatile components that form the backbone of many iOS apps. Mastering them will allow you to create flexible, responsive interfaces for displaying collections of data.

Additional Resources

Exercises

  1. Create a Pinterest-style staggered grid layout using collection view
  2. Implement a photo gallery with zooming capabilities when cells are tapped
  3. Build a calendar view using collection view with month sections and day cells
  4. Create a collection view with multiple sections, each with a different layout
  5. Implement drag-and-drop reordering in a collection view of tasks or notes

By practicing these exercises, you'll develop a strong foundation in UICollectionView and be ready to implement complex interfaces in your own apps.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)