Modern Collection Views with Compositional Layouts
In this tutorial, you’ll learn how to build beautiful, modern UICollectionView layouts using iOS 13’s new declarative UICollectionViewCompositionalLayout API. By Tom Elliott.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Modern Collection Views with Compositional Layouts
25 mins
The Power of Groups
About now you’re probably thinking: Columns and insets are good and all, but that was already easy with UICollectionViewFlowLayout
. Show me something cool! Don’t worry,UICollectionViewCompositionalLayout
has your back.
One of the fanciest parts of the album detail view in the stock Photos apps is the effect with photos of different sizes. Building this layout is surprisingly easy once you realize that you can nest Groups within other Groups. This is the effect you’re shooting for.
At first glance, this looks very complicated, but you can break it down into four distinct layouts, two of which are just mirrored examples of each other:
- A full width photo.
- A ‘main’ photo with a pair of vertically stacked smaller photos.
- Three smaller photos in a row.
- The reverse of the second style.
Replace everything before let groupSize = ...
in generateLayout()
with the following:
// We have three row styles
// Style 1: 'Full'
// A full width photo
// Style 2: 'Main with pair'
// A 2/3 width photo with two 1/3 width photos stacked vertically
// Style 3: 'Triplet'
// Three 1/3 width photos stacked horizontally
// First type. Full
let fullPhotoItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(2/3)))
fullPhotoItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 2,
trailing: 2)
The first layout type is simple: A single image that is the full width of the screen. You make this by creating a single item with a fractional width of 1.0 (full-width) and a height 2/3 of the width.
Below this, add the second layout type:
// Second type: Main with pair
// 3
let mainItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(2/3),
heightDimension: .fractionalHeight(1.0)))
mainItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 2,
trailing: 2)
// 2
let pairItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.5)))
pairItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 2,
trailing: 2)
let trailingGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1/3),
heightDimension: .fractionalHeight(1.0)),
subitem: pairItem,
count: 2)
// 1
let mainWithPairGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(4/9)),
subitems: [mainItem, trailingGroup])
The second group consists of a mainItem
and a pair of smaller items. The smaller items are themselves a Group laid out vertically, contained within a horizontally laid out Group consisting of the main item and the Group containing the pair of smaller items.
The math here gets a little tricky. You have to remember that each size is relative to its parent. You may find it easier to start from the outer layer and work backward — bottom to top in the code.
- The outer Group,
mainWithPairGroup
, should be full width so it has a fractional width of1.0
. The height of the main item dictates its height. This is 2/3 the width of the screen, so the height needs to be 2/3 of the height of the full-width photo from the first layout. If you remember high school math, you’ll know that 2/3 of 2/3 is 4/9! - The trailing Group containing the two vertically stacked smaller items should then be 1/3rd the width and the full height of its containing Group. Each of the smaller items should be the full width of the trailing Group and half its height.
- Finally, the main item should be 2/3rd the width and the full height of its containing Group.
Next, add the triplet group below the second group:
// Third type. Triplet
let tripletItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1/3),
heightDimension: .fractionalHeight(1.0)))
tripletItem.contentInsets = NSDirectionalEdgeInsets(
top: 2,
leading: 2,
bottom: 2,
trailing: 2)
let tripletGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(2/9)),
subitems: [tripletItem, tripletItem, tripletItem])
This third Group contains three horizontally laid out photos across the width. This means the Group should have a full fractional width, but a height 1/3 that of the full-sized photo — or 2/9. Within the Group, each Item should be the full height and 1/3 of the width.
And now, add the fourth and final style:
// Fourth type. Reversed main with pair
let mainWithPairReversedGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(4/9)),
subitems: [trailingGroup, mainItem])
Given the fourth layout is the inverse of the second, you can achieve this easily by changing the order of subitems in the Group. :]
To finish the new layout, replace the groupSize
, group
and section
definitions with the following:
let nestedGroup = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(16/9)),
subitems: [
fullPhotoItem,
mainWithPairGroup,
tripletGroup,
mainWithPairReversedGroup
]
)
let section = NSCollectionLayoutSection(group: nestedGroup)
Here, you add the four Group types to a container Group of vertically stacked items. These should take up the full width and, if you run through the numbers, a height equivalent to 1 and 7/9 the normal height or 16/9.
Phew! That was a lot of layout. But each part is simple when you break it down, and you didn’t have to build any complex logic for this layout to support multiple devices or orientations. Go on, build and run the project. Give it a whirl and try it out. Try rotating the device too!
Adding Supplementary Items
A common feature of modern apps is to add additional context to the items in a collection view at the Item or Section level. For example, an item representing an app on your home screen may have a little red badge on the top-right corner showing the number of unread notifications. Or, a section in the Contacts app may have a header indicating which letter of the alphabet this section is displaying.
UICollectionViewCompositionalLayout
provides the Supplementary Items API for exactly these types of additional items. You’ll now use it to add a badge to a photo that indicates it’s syncing with a cloud storage system.
In AlbumDetailViewController.swift, navigate to configureCollectionView()
and add the following after registering the photo item cell:
collectionView.register(
SyncingBadgeView.self,
forSupplementaryViewOfKind: AlbumDetailViewController.syncingBadgeKind,
withReuseIdentifier: SyncingBadgeView.reuseIdentifier)
This code tells the collection view that it should use the SyncingBadgeView
class when requested to add a supplementary view of a certain kind with the relevant reuse identifier. You have probably come across reuse identifiers before, but the supplementary view kind may be new to you. Like a reuse identifier, this is simply a string that acts as a key to tell UIKit which type of view you want to add.
If you’re interested in checking out the implementation of the syncing badge view, open SyncingBadgeView.swift in the SharedViews group. It’s just a simple UICollectionReusableView
subclass which displays an image.
Head back to AlbumDetailViewController.swift. Next, you need to configure your data source with a SupplementaryViewProvider
. This is a simple method that, when passed a collection view, kind and index path, returns an optional view. Add the following to configureDataSource()
, directly after creating the data source:
dataSource.supplementaryViewProvider = {
(
collectionView: UICollectionView,
kind: String,
indexPath: IndexPath)
-> UICollectionReusableView? in
// 1
if let badgeView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: SyncingBadgeView.reuseIdentifier,
for: indexPath) as? SyncingBadgeView {
// 2
let hasSyncBadge = indexPath.row % Int.random(in: 1...6) == 0
badgeView.isHidden = !hasSyncBadge
return badgeView
} else {
fatalError("Cannot create new supplementary")
}
}
This code:
- Asks the collection view to dequeue a supplementary view of the right kind.
- Determines if a photo is syncing, in this case at random, and hides the badge if you don’t want to display it.
Now there’s one more part you need to add to show the syncing view. You need to tell the collection view where to show it!
At the top of generateLayout()
, add the following code:
// Syncing badge
let syncingBadgeAnchor = NSCollectionLayoutAnchor(
edges: [.top, .trailing],
fractionalOffset: CGPoint(x: -0.3, y: 0.3))
let syncingBadge = NSCollectionLayoutSupplementaryItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(20),
heightDimension: .absolute(20)),
elementKind: AlbumDetailViewController.syncingBadgeKind,
containerAnchor: syncingBadgeAnchor)
This code defines the layout for the syncing view as anchored 30% from the top and trailing edges with an absolute width and height of 20 points.
Finally, you need to add this layout to any item that you want to display the syncing badge. In our case, that is to any of the items representing photos. NSCollectionLayoutItem
has a convenience initializer that accepts an array of supplementary items. Update fullPhotoItem
to take advantage of this initializer as follows:
let fullPhotoItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalWidth(2/3)),
supplementaryItems: [syncingBadge])
Build and run the project. Remember, the supplementary syncing item is visible randomly, so you may not see it every time!