How to Build a Carousel UI with Paging Support in iOS

Build a carousel-like UI with paging in iOS using UICollectionViewCompositionalLayout with orthogonalScrollingBehavior.

Setting Up the UICollectionView Layout

Use UICollectionViewCompositionalLayout to define the layout with paging behavior:

private func createCarouselLayout() -> UICollectionViewLayout {
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0)
    )
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)

    let groupSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0)
    )
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .groupPaging

    return UICollectionViewCompositionalLayout(section: section)
}

The orthogonalScrollingBehavior property controls how the carousel scrolls. .groupPaging snaps each card into place. .continuous enables free scrolling. .groupPagingCentered snaps with the current card centered and adjacent cards peeking from edges.

Configuring the Diffable Data Source

Use UICollectionViewDiffableDataSource for clean data management:

enum Section {
    case main
}

struct Trip: Hashable {
    let id: String
    let city: String
    let country: String
    let price: Int
    let totalDays: Int
    let imageName: String
    var isLiked: Bool
}

var dataSource: UICollectionViewDiffableDataSource<Section, Trip>!

func configureDataSource() {
    dataSource = UICollectionViewDiffableDataSource<Section, Trip>(
        collectionView: collectionView
    ) { collectionView, indexPath, trip in
        let cell = collectionView.dequeueReusableCell(
            withReuseIdentifier: "TripCell",
            for: indexPath
        ) as! TripCell
        cell.cityLabel.text = trip.city
        cell.countryLabel.text = trip.country
        cell.priceLabel.text = "$(trip.price)"
        cell.daysLabel.text = "(trip.totalDays) days"
        cell.imageView.image = UIImage(named: trip.imageName)
        cell.layer.cornerRadius = 12
        return cell
    }
}

func updateSnapshot(with trips: [Trip]) {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Trip>()
    snapshot.appendSections([.main])
    snapshot.appendItems(trips)
    dataSource.apply(snapshot, animatingDifferences: false)
}

Handling the Like Button with Delegation

Define a delegate protocol in the cell file:

protocol TripCellDelegate: AnyObject {
    func didTapLikeButton(in cell: TripCell)
}

class TripCell: UICollectionViewCell {
    @IBOutlet var imageView: UIImageView!
    @IBOutlet var cityLabel: UILabel!
    @IBOutlet var countryLabel: UILabel!
    @IBOutlet var priceLabel: UILabel!
    @IBOutlet var daysLabel: UILabel!
    @IBOutlet var likeButton: UIButton!

    weak var delegate: TripCellDelegate?

    var isLiked = false {
        didSet {
            let image = isLiked ? UIImage(systemName: "heart.fill") : UIImage(systemName: "heart")
            likeButton.setImage(image, for: .normal)
        }
    }

    @IBAction func likeButtonTapped(_ sender: UIButton) {
        delegate?.didTapLikeButton(in: self)
    }
}

Adopt the protocol in the view controller:

extension TripViewController: TripCellDelegate {
    func didTapLikeButton(in cell: TripCell) {
        guard let indexPath = collectionView.indexPath(for: cell),
              let trip = dataSource.itemIdentifier(for: indexPath) else { return }

        trips[indexPath.row].isLiked.toggle()
        cell.isLiked = trips[indexPath.row].isLiked
    }
}

Assign the delegate in the data source configuration:

cell.delegate = self

Designing the Card Interface

Configure each card with rounded corners and shadow for depth:

cell.contentView.layer.cornerRadius = 12
cell.contentView.layer.masksToBounds = true
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOpacity = 0.2
cell.layer.shadowOffset = CGSize(width: 0, height: 4)
cell.layer.shadowRadius = 8
cell.layer.masksToBounds = false

Use NSDirectionalEdgeInsets on the layout item for spacing between cards. This approach is cleaner than adding spacer views or adjusting frame calculations.

Peek-and-Scale Carousel Variation

Create a peek-and-scale carousel by reducing the group width:

let groupSize = NSCollectionLayoutSize(
    widthDimension: .fractionalWidth(0.85),
    heightDimension: .fractionalHeight(0.9)
)

Set orthogonalScrollingBehavior to .groupPagingCentered. Adjacent cards peek from the edges, inviting users to scroll. Add a scale transform in scrollViewDidScroll for a depth effect on the focused card.

Best Practices for Carousel Interfaces

Use compositional layouts for maximum flexibility. NSCollectionLayoutSize supports fractional dimensions, enabling adaptive layouts across screen sizes.

Manage cell state in the data model, not the view. Store isLiked in the Trip struct and configure cells from data. Cell reuse corrupts view-level state.

Apply content insets via NSDirectionalEdgeInsets on layout items, not through cell constraints. Layout-level insets provide consistent spacing and respect the layout's scrolling behavior.

Optimize images for carousel display. Large images degrade scrolling performance and memory usage. Use appropriately sized thumbnails and cache images with NSCache.

Test on different screen sizes. Fractional widths and heights ensure the layout adapts. Verify card proportions on both iPhones and iPads in portrait and landscape.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

What is UICollectionViewCompositionalLayout? UICollectionViewCompositionalLayout is a UIKit layout class, available since iOS 13, that builds collection view layouts from items, groups and sections. It is part of iOS.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

All articles