Today you are going to learn how to implement a collection view in iOS 13 using diffable data source with the newly Compositional Layout.
I was lucky to be part of the 5000 developers who attended this year’s WWDC19 and I would like to share some of what I learned with you.
When I left for the conference I had just finished working on a new iPad layout at work where I had to create a custom layout using the UICollectionViewFlowDelegate. Here it is:
I was blown away when they announced the new Compositional Layout at WWDC19. It would have made my life so much easier to have it earlier, but now we can use it and focus on more complex issues.
Background
Up until now, we were used to implementing the CollectionView delegate methods to set and update our data source when new data was fetched from a remote server or even a local update. We would use the reloadData() or performBatchUpdates() methods.
The problem with both calls are that the Reload() is often used incorrectly, where it is called every time the data source has changed and causes the whole collection view to refresh and causes the UI to look jumpy.
This is where performBatchUpdates used to come in place. It lets you update the newly added data into the collection view, “The Diffs”, but we also had to call it on the Main Thread and make sure the changes were called in the right order which was quite annoying and error prone
In the Diffable Data Source world we create a snapshot that always reflects the current data model.
In Apple’s own words:”Use UI Data Sources to simplify updating your table view and collection view items using automatic diffing.High fidelity, quality animations of set changes are automatic and require no extra code!This improved data source mechanism completely avoids synchronization bugs, exceptions, and crashes!Learn about this simplified data model that uses on identifiers and snapshots so that you can focus on your app’s dynamic data and content instead of the minutia of UI data synchronization.”
Bottom line, this Diffable mechanism will reduce the many errors we encounter while updating our data model, the less states to keep track of, the fewer bugs tend to appear in our app, plus we get out of the box animations.
So how does this new Diffable Data Source work you may ask? Well, it basically consists of two main parts.
1) Configuring the UICollectionView Diffable DataSource2) Creating the NSDiffable DataSource Snapshot
This is what we are going to build, I’ve add a custom cell class, and some UIView extensions, so make sure to clone the project.
Lets Dive In
Lets open Xcode and create a new Project -> Single View App, name it and save it wherever you desire, rename ViewController.swift to CollectionViewController.swift using the refactor option.
Create a new file and name it Contact. This will be our Data Model and it will consist of the contact name and image name.
struct Contact: Hashable {
let name: String
let image: String
}
As you can see, our Model conforms to the Hashable protocol which is needed for our diffable data source to be able to uniquely identify it.
Back into CollectionViewController Just above the class declaration, add the following type-aliases, It will help us reduce some code typing.
fileprivate typealias UserDataSource = UICollectionViewDiffableDataSource<ViewController.Section, Contact>fileprivate typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<ViewController.Section, Contact>
In CollectionViewController just above viewDidLoad add the following properties:
//MARK: - Properties// Here we add a cell identifier.
let cellId = "cellId"// We then create our Contact array which will hold our contacts data.
private var contacts = [Contact]()// We create an instance of our contact data source
private var dataSource: ContactDataSource!// Finally our collectionView which we will setup later.
private var collectionView: UICollectionView! = nil
Add the following extensions
//MARK: - Collection View Delegateextension CollectionViewController: UICollectionViewDelegate { fileprivate enum Section {
case main
} func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { collectionView.deselectItem(at: indexPath, animated: true) guard let contact = dataSource.itemIdentifier(for: indexPath) else { return} print(contact)
}}
- The Section enum represents the collection view sections. Here we are setting up the number of sections that the collection view will have which is currently one and it is called “main”. The section enum conforms to the Hashable protocol as well.
2. The didSelectItemAt delegate which is called upon selecting the specified cell.
//MARK: - Collection View Setupextension CollectionViewController { private func createData() { } private func createLayout() -> UICollectionViewLayout { } private func configureHierarchy() { } private func configureDataSource() { }}
We are now going to go over each function and implement the logic necessary to make this collection view rock!
Dummy Data
This simple function creates dummy contacts so we can populate our cells.
private func createData() {
for i in 0..<10 {
contacts.append(Contact(name:”Contact \(i)”, image: “”))
}
}
Compositional Layout
The new compositional layout is remarkably easy to use, it constitutes of four steps.
Visual illustration of the new Compositional Layout Hierarchy
private func createLayout() -> UICollectionViewLayout {
//1
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 8, trailing: 16)//2
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.25)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])//3
let section = NSCollectionLayoutSection(group: group)//4
let layout = UICollectionViewCompositionalLayout(section: section) return layout}
1. We start by creating the item size using NSCollectionLayoutSize, the size will be the actual cell dimension.
We then construct the item itself using the size and add some padding using the item contentInsets property.
2. Then we create our group. Similarly, we first give the group its size with NSCollectionLayoutSize. This will ensure the group keeps the proper dimensions once it is populated in the layout.
Once we have our group size we create the actual group by choosing the axis (Vertical / Horizontal) the groupSize from above and its sub items it will hold.
3-4. Finally we create a section using the group constant from above and return it as our layout.
Configure Hierarchy
private func configureHierarchy() {//1
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())//2
collectionView.delegate = self//3
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground//4
collectionView.register(ContactCell.self, forCellWithReuseIdentifier: cellId)
view.addSubview(collectionView)}
1. We assign to our collectionView with a UICollectionView initializer that gets the frame size and layout.
2. Pass our self as a delegate.
3. Set the autoresize property to flexible width & height. This ensures us that when a view’s bounds change the view automatically resizes its subviews accordingly, and lastly give a default system background color.
4. We register our collection view cell and add the collection view onto our view.
Configure Data Source
private func configureDataSource() { dataSource = ContactDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, contact) -> ContactCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellId, for: indexPath) as! ContactCell cell.contact = contact
return cell}) let snapshot = DataSourceSnapshot()
snapshot.appendSections([Section.main])
snapshot.appendItems(self.contacts)
dataSource.apply(snapshot, animatingDifferences: false)}
In the above function we create our Data source using the ContactDataSource alias. This closure replaces the cellForItemAtIndexPath delegate method we were used to conforming to in iOS 12 and below.
Then we create an instance of our DataSourceSnapshot which will manage our model state.
We append it an array of sections, which we have only one of.
Then we append our contacts array into our snapshot.
Finally we apply the changes. This will Load/Update the data into our collection view.
CollectionViewController Class Overview
Build and run.
In conclusion
You have learned how to build a collection view using Apple’s newest Compositional Layout and Diffable Data Source.
I hope you enjoyed this tutorial, if you have any questions or ways for me to improve it let me know.
In part 2 I will show you how to populate the collection view using a network call. We will go over fetching and deleting our data model.
Where to Go From Here?
if you are interested in more complex layouts for your collection view using Compositional Layouts I suggest you watch this WWDC talk: