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.