Blog

How to get started with UICollectionViewCompositional Layout

Date

16th March 2023

Read

8 min

Creator

Rajat Kumar

If you’re looking for a faster and simpler way to create dynamic and flexible layouts for your iOS app’s collection views‚ then UICollectionViewCompositionalLayout might be just what you need. With its declarative API and powerful features‚ this layout system allows you to easily create complex and customisable layouts in just a few lines of code. In this blog post‚ I’ll be going through a quick-start guide to help you get started with UICollectionViewCompositionalLayout by creating a simple paging layout. So let’s dive in and get started!

The Problem

The objective was to design and develop a user-friendly vertical scrolling list of paging layouts‚ which should allow users to navigate through the list by scrolling horizontally. The design should provide an easy-to-use interface that allows users to easily identify the next and previous items in the list‚ which should be partially visible even when they are not currently in focus. It had to be both efficient and responsive‚ allowing for a seamless user experience across all devices. We had decided to start the project using SwiftUI and wanted to keep components as SwiftUI native as possible and reduce the use of UIKit.

We decided to use an HStack with DragGesture to mimic the effect that a UICollectionView’s horizontal layout provides. This worked fine standalone but we needed 3 of the horizontal layouts stacked vertically in a ScrollView. However‚ When we start dragging within the HStack‚ the drag gesture seems to capture the touch events and prevent them from propagating up to the ScrollView. This caused the ScrollView to stop scrolling and make it difficult to scroll across the content especially when dragged along the angles(orthogonal) in both directions. Now‚ it would have worked fine in case we only needed to show one carousel outside a ScrollView.

[object Object]

What is UICollectionViewCompositionalLayout?

We couldn’t move forward with the gesture issues in SwiftUI ScrollView‚ so we explored using UICollectionView.

At WWDC 2019‚ Apple introduced a simple and powerful API for building complex layouts. UICollectionViewCompositionalLayout was made to simplify the collection view layouts using a declarative way without the need for nested collection views to maximise customization. I’m not going to talk about UICollectionView in detail‚ but here are some helpful links:

https://developer.apple.com/documentation/uikit/uicollectionviewhttps://www.kodeco.com/18895088-uicollectionview-tutorial-getting-started

To build a similar layout using a CollectionView‚ we would have had vertical scrolling sections with a horizontal UICollectionView in each section. Here’s the interesting part the UICollectionViewCompositionalLayout lets you build the layout without adding nested UICollectionViews. It does that by introducing Groups.

What is a Group?

NSCollectionLayoutGroup is an additional layer between sections and items. It determines how the items in a collection view lay out in relation to each other Horizontal‚ vertical or custom.Each group specifies its own size in terms of a width dimension and a height dimension. Groups can express their dimensions relative to their container‚ as an absolute value‚ or as an estimated value that might change at runtime‚ like in response to a change in system font size.Because a group is a subclass of NSCollectionLayoutItem‚ it behaves like an item. You can combine a group with other items and groups into more complex layouts.

[object Object]

Implementation

Enough of the theoretical part‚ let’s see how we can achieve the behaviour we want with the help of the layout.

Let’s define our item‚ we want our item to have same width and height as the containing Group. So we provide it as .fractionalWidth(1.0) and .fractionalHeight(1.0). You can also provide the absolute or estimated size of the item.

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0)‚ heightDimension: .fractionalHeight(1.0))let item = NSCollectionLayoutItem(layoutSize: itemSize)

Next‚ we define a Group Size. For the width of the group we provide it as a fraction value of 0.90 with respect to the section. Since we want the item to fill 90% of the width of the section so it makes the last and next item slightly visible the item inside the group will fill the group completely so we don’t have to make any changes there.We provide an absolute height for the group‚ it can be anything but we’d like the item to have a square appearance so provided it with height equal to the screen width minus the left and right margins.

let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.92)‚ heightDimension: .absolute(UIScreen.main.bounds.width - /* left and right margins */))

To define a group we use horizontal(layoutSize:repeatingSubitem:count:) pretty straightforward‚ we want our group to be horizontal and it will have one item.


let group = NSCollectionLayoutGroup.horizontal( layoutSize: groupSize‚ repeatingSubitem: item‚ count: 1)

Next we create our section with the group and initialise our layout with the section

let section = NSCollectionLayoutSection(group: group)let layout = UICollectionViewCompositionalLayout(section: section)

Now that we know how sections and groups work‚ let’s go ahead and put it together. We’ll create a static var in extension of UICollectionViewCompositionalLayout‚ horizontalPagingLayout

extension UICollectionViewCompositionalLayout { static var horizontalPagingLayout: UICollectionViewCompositionalLayout { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0)‚ heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let side = UIScreen.main.bounds.width - /* left and right margins */) let groupFractionalWidth = 0.92 let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(groupFractionalWidth)‚ heightDimension: .absolute(side)) let group = NSCollectionLayoutGroup.horizontal( layoutSize: groupSize‚ repeatingSubitem: item‚ count: 1) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section) return layout }}

Let’s setup our model‚ we’re going to use images from our asset library and will put that in a Section class and make an array of Section objects. Also‚ going to create a custom UICollectionViewCell that has an imageView and property named image which when set will set the image of UIImageView.

let someCatImages: [String] = (0…7).map { "cat($0)" }class Section { var images: [String] init(images: [String]) { self.images = images }}extension Section { static var allSections: [Section] = [ Section(images: someCatImages)‚ Section(images: someCatImages)‚ Section(images: someCatImages) ]}

class ImageCollectionViewCell: UICollectionViewCell { @IBOutlet weak var contentImageView: UIImageView! var image: String? { didSet { if let image = image { self.contentImageView.image = UIImage(named: image) } } }}

After that we’ll setup our ViewController with a collectionView.

import UIKitclass CompositionalLayoutViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! let sections = Section.allSections override func viewDidLoad() { super.viewDidLoad() collectionView.showsVerticalScrollIndicator = false collectionView.collectionViewLayout = UICollectionViewCompositionalLayout.horizontalPagingLayout }}extension CompositionalLayoutViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { sections.count } func collectionView(_ collectionView: UICollectionView‚ numberOfItemsInSection section: Int) -> Int { sections[section].images.count } func collectionView(_ collectionView: UICollectionView‚ cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCollectionViewCell"‚ for: indexPath) as! ImageCollectionViewCell cell.image = sections[indexPath.section].images[indexPath.item] return cell }}

We’re almost done‚ let’s see how it looks.

[object Object]

It loads all the items but all items are appearing vertically. To change that we need set the property of section orthogonalScrollingBehaviour to .groupPagingCentered which will make the items in section scroll perpendicular to the scroll direction of CollectionView and its going to centre the element in the middle of screen. It looks much better now. Let’s set some spacings on group and section so there’s spacing between them.We’re going to use contentInsets property on group and section to get the spacing.

section.contentInsets = NSDirectionalEdgeInsets(top: 10.0‚ leading: 0.0‚ bottom: 0.0‚ trailing: 0.0)group.contentInsets = NSDirectionalEdgeInsets(top: 0‚ leading: 8.0‚ bottom: 0‚ trailing: 8.0)

Voila!! we have our desired behaviour.

[object Object]

Wait‚ the most interesting part 🙂

We can use our section’s visibleItemsInvalidationHandler property and some math (borrowed from StackOverflow) to get some nice animation. We’ll calculate the horizontal distance of each visible item from the center and set the scaling so that when an item is not in focus its smaller and goes to original size when it is in the center of the container.

section.visibleItemsInvalidationHandler = { (items‚ offset‚ environment) in items.forEach { item in let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0) let minScale: CGFloat = 0.7 let maxScale: CGFloat = 1.0 let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width)‚ minScale) item.transform = CGAffineTransform(scaleX: scale‚ y: scale) } }

And the final result…

[object Object]

What’s next

That concludes an introduction to UICollectionViewCompositionalLayout. In later articles of the series I’ll be looking at other APIs we can use to improve the code. I’ll be pairing the layout up with UICollectionViewDiffableDataSource to add items to the CollectionView with less code and some nice benefits it provides. I’ll also be looking at how we can use CollectionView with Compositional layout in SwiftUI.