
In this WWDC 2019, Apple introduced many great enhancements and new features such as SwiftUI, Core ML 3 framework or Combine. But I’ve got my eyes on UICollectionViewCompositionalLayout, a new extension of UICollectionViewLayout.
In my opinion, UICollectionView played an important role in app development. It’s not that easy to customize UICollectionView to create a complicated layout. For example, if you want to create a fancy showcase for a company products, you usually find a third-party library to make a complex layout for the collection view, otherwise, tons of codes should be written by yourself. Luckily, Apple now gives us UICollectionViewCompositionalLayout to make the job much easier.
The purpose of this post is to share my experiment about UICollectionViewCompositionalLayout. My aim is to create a really simple application which shows you how easy was that to customize the layout with just a little amount of code involved.
Let’s start with a setup for UICollectionView
1 2 3 4 5 6 7 8 |
private func setupCollectionView() { collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: menuLayout()) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.register(UINib(nibName: TextCell.nibName, bundle: nil), forCellWithReuseIdentifier: TextCell.reuseIdentifier) collectionView.backgroundColor = .white collectionView.delegate = self view.addSubview(collectionView) } |
Next, I want to create a menu for my simple application. A function called menuLayout() will return a UICollectionViewCompositionalLayout for the UICollectionView. I set the values of ‘fractionalWidth’ equal to 1.0, in oder to make a list of menu items (which looks like a UITableView).
1 2 3 4 5 6 7 8 9 10 |
private func menuLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44.0)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section) return layout } |
As you can see, with just a few code, the layout is fully set up. We need to give the collection view a data source by using UICollectionViewDiffableDataSource for the collection view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func setupDataSource() { dataSource = UICollectionViewDiffableDataSource <Section, Menu>(collectionView: collectionView, cellProvider: { (collectionView: UICollectionView, indexPath: IndexPath, item: Menu) -> UICollectionViewCell? in guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TextCell.reuseIdentifier, for: indexPath) as? TextCell else { fatalError("cannot create cell") } cell.setText(text: item.rawValue) return cell }) var dataSourceSnapshot = NSDiffableDataSourceSnapshot<Section, Menu>() dataSourceSnapshot.appendSections([Section.menu]) dataSourceSnapshot.appendItems(Array(Menu.allCases)) dataSource.apply(dataSourceSnapshot, animatingDifferences: false) } |
And don’t forget to create Section Identifier Type and Item Identifier Type for the data source : a ‘Menu’ enum with raw value type as string, it should be case iterable. Just like this:
1 2 3 4 5 6 7 8 9 10 |
/// Section Identifier Type enum Section { case menu } /// Item Identifier Type enum Menu: String, CaseIterable { case EstimatedGrid = "Estimated Grid" case NestedGroup = "Nested Grouping" case Sections = "Sections" } |
Let’s run the program and see what we have here
It looks promising, isn’t it?
Above is a simple example for us to get started with UICollectionViewCompositionalLayout. If we want to make a fancy layout, like the Appstore for instance, we need to take a deep look into it.
NSCollectionLayoutItem
NSCollectionLayoutItem is for setting up the item in the group, it decides what is the size of an item, or how to calculate the item dimensions. NSCollectionLayoutSize is needed to initialize NSCollectionLayoutItem. By this, we can set up the size, edge spacing and content insets.
For example, I add a separator view and giving these items some spacing by the following code
1 2 |
let item = NSCollectionLayoutItem(layoutSize: itemSize) item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: .fixed(10), trailing: nil, bottom: .fixed(10)) |
It looks much better after we adjusted the spacing between items
If you set NSCollectionLayoutSize.heightDimension = .fractionalWidth(0.5), it means the height of the item will be equal to half of group width and and vice versa. Or if you use estimated, it quickly calculates the size of item, group to fit with your setup. You may want to play around with these variants yourself to find more interesting things.
Here is an example of a grid view with estimated height dimensions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private func createLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(50)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: nil, top: .fixed(10), trailing: .fixed(10), bottom: .fixed(10)) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section) return layout } |
Estimated Grid Collection View
NSCollectionLayoutGroup
NSCollectionLayoutGroup is extending NSCollectionLayoutItem and you can add into it as many items as you want to.
In order to initialize NSCollectionLayoutGroup, you have to define at least 3 things:
- The direction is horizontal or vertical.
- Size of group (layoutSize).
- How many items in the group (subitem).
The layout will render number of groups, depending on how many sub items that we defined above and how many items that we have in the data source.
For example, we have one group in the section, and group’s sub-items has two items, datasource contains ten items, then five groups will be drawn. interItemSpacing variable supplies additional spacing between items along the layout axis of the group.
Horizontal direction
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Specifies a group that will have N items equally sized along the horizontal axis. use interItemSpacing to insert space between items // // +------+--+------+--+------+ // |~~~~~~| |~~~~~~| |~~~~~~| // |~~~~~~| |~~~~~~| |~~~~~~| // |~~~~~~| |~~~~~~| |~~~~~~| // +------+--+------+--+------+ // ^ ^ // | | // +-----------------------+ // | Inter Item Spacing | // +-----------------------+ // |
Vertical direction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Specifies a group that will have N items equally sized along the vertical axis. use interItemSpacing to insert space between items // +------+ // |~~~~~~| // |~~~~~~| // |~~~~~~| // +------+ // | |<--+ // +------+ | // |~~~~~~| | +-----------------------+ // |~~~~~~| +----| Inter Item Spacing | // |~~~~~~| | +-----------------------+ // +------+ | // | |<--+ // +------+ // |~~~~~~| // |~~~~~~| // |~~~~~~| // +------+ // |
Other than these 2 default directions, we can also create our own custom variant by using NSCollectionLayoutGroupCustomItem. The crucial point is that we can have a group nested inside another group. Following is an example of a complex layout with multiple groups.
I added randomly some nested group in the following code and here is the result
Nested group
NSCollectionLayoutSection
We can have multiple sections in a collection view, each section can have one or more groups with different directions.
Let’s play around with the section and its variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private func createLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { sectionNumber, env -> NSCollectionLayoutSection? in switch Section(rawValue: sectionNumber) { case .first: return self.gridSection() case .second: return self.listSection() case .third: return self.gridSection() default: return nil } } } |
1 2 3 4 5 6 7 8 9 10 11 |
//Dummy data var snapshot = NSDiffableDataSourceSnapshot<Section, Int>() snapshot.appendSections([.first]) snapshot.appendItems(Array(1..<5)) snapshot.appendSections([.second]) snapshot.appendItems(Array(6..<9)) snapshot.appendSections([.third]) snapshot.appendItems(Array(10..<100)) dataSource.apply(snapshot, animatingDifferences: false) |
And here is the result:
UICollectionViewCompositionalLayout
UICollectionViewCompositionalLayout is the “main character”. We can initialize it with NSCollectionLayoutSection or with closure, this closure will be called when layout needs information about the section. Initializing it with UICollectionViewCompositionalLayoutConfiguration, we can set space between sections and scroll direction. Using UICollectionViewCompositionalLayout you can set up different behavior for each sections.
Conclusion
Apple made me WOW when they first introduced UICollectionViewCompositionalLayout. With this new layout, developers can create all the custom layouts without miserably finding a third-party library. In the next blog post, I will try to “copy” the layout of Apps tab in the iPhone Appstore.
Stay tuned for more!
Demo: Source Code
Reference: Advances in Collection View Layout