In last week’s article, we learned how to leverage the NSDiffableDataSourceSectionSnapshot
to create an expandable list in a single-section collection view. In this article, we will pick up where we left off by modifying last week’s sample app to display the same set of model objects in a multi-section collection view.
Following animated GIF showcases what we going to build in this article.
If you have not gone through my last week’s article — “Building an Expandable List Using UICollectionView: Part 1“, I strongly encourage you to check it out before proceeding.
That said, let’s get right into it!
Defining Required Data Types
The data types required for the sample app are basically the same as part 1. The only difference is that we no longer need the Section
enum as the section identifier type. This is because the Section
enum is only suitable for representing a collection view with a fixed number of sections.
The following are all the data types and model objects we need for the sample app:
// Section identifier type & header cell data type
struct HeaderItem: Hashable {
let title: String
let symbols: [SFSymbolItem]
}
// Symbol cell data type
struct SFSymbolItem: Hashable {
let name: String
let image: UIImage
init(name: String) {
self.name = name
self.image = UIImage(systemName: name)!
}
}
// Item identifier type
enum ListItem: Hashable {
case header(HeaderItem)
case symbol(SFSymbolItem)
}
// The model objects to show
let modelObjects = [
HeaderItem(title: "Communication", symbols: [
SFSymbolItem(name: "mic"),
SFSymbolItem(name: "mic.fill"),
SFSymbolItem(name: "message"),
SFSymbolItem(name: "message.fill"),
]),
HeaderItem(title: "Weather", symbols: [
SFSymbolItem(name: "sun.min"),
SFSymbolItem(name: "sun.min.fill"),
SFSymbolItem(name: "sunset"),
SFSymbolItem(name: "sunset.fill"),
]),
HeaderItem(title: "Objects & Tools", symbols: [
SFSymbolItem(name: "pencil"),
SFSymbolItem(name: "pencil.circle"),
SFSymbolItem(name: "highlighter"),
SFSymbolItem(name: "pencil.and.outline"),
]),
]
Configuring Collection View & Performing Cell Registration
The code to configure the collection view and perform cell registration is exactly the same as part 1. Therefore, feel free to refer to part 1 if you need a more detailed explanation of the following code.
// MARK: Create list layout
let layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
// MARK: Configure collection view
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: listLayout)
view.addSubview(collectionView)
// Make collection view take up the entire view
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 0.0),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
// MARK: Cell registration
let headerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, HeaderItem> {
(cell, indexPath, headerItem) in
// Set headerItem's data to cell
var content = cell.defaultContentConfiguration()
content.text = headerItem.title
cell.contentConfiguration = content
// Add outline disclosure accessory
// With this accessory, the header cell's children will expand / collapse when the header cell is tapped.
let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header)
cell.accessories = [.outlineDisclosure(options:headerDisclosureOption)]
}
let symbolCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SFSymbolItem> {
(cell, indexPath, symbolItem) in
// Set symbolItem's data to cell
var content = cell.defaultContentConfiguration()
content.image = symbolItem.image
content.text = symbolItem.name
cell.contentConfiguration = content
}
Initializing Data Source
As mentioned earlier, we are no longer able to use the Section
enum (defined in part 1) as the data source section identifier type. In order to represent a collection view with a dynamic number of sections, we must use the model objects data type (HeaderItem
) as section identifier type.
By changing the section identifier type from Section
to HeaderItem
, means that we are changing the data type that represents a section in the collection view.
At this point, you might be wondering, does changing the section identifier type affect the cell provider closure? The answer is”No”.
This is because the main purpose of the cell provider closure is to dequeue a cell based on the item identifier type, it has nothing to do with the section identifier type. Since we are still using ListItem
as the item identifier type, the logic to dequeue a cell should stay the same.
// Change section identifier type from "Section" to "HeaderItem"
dataSource = UICollectionViewDiffableDataSource<HeaderItem, ListItem>(collectionView: collectionView) {
(collectionView, indexPath, listItem) -> UICollectionViewCell? in
// Code here is exactly the same as part 1
switch listItem {
case .header(let headerItem):
// Dequeue header cell
let cell = collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration,
for: indexPath,
item: headerItem)
return cell
case .symbol(let symbolItem):
// Dequeue symbol cell
let cell = collectionView.dequeueConfiguredReusableCell(using: symbolCellRegistration,
for: indexPath,
item: symbolItem)
return cell
}
}
Setting Up Snapshots
In order to display our model objects in a multi-section collection view, we must reconstruct the data source snapshot (NSDiffableDataSourceSnapshot
) and section snapshot (NSDiffableDataSourceSectionSnapshot
).
If you’re unfamiliar with the concept of snapshots, you can refer to the “Understanding Different Kind of Snapshots” section in part 1.
The following diagram illustrates the snapshots structure that we need to construct.
For the sake of comparison, here’s the snapshot structure for a single-section collection view.
As can be seen, we need to construct a data source snapshot with 3 collection view sections. Within each collection view section will contain a single-section section snapshot. In other words, we will need a total of 3 section snapshots for the entire collection view.
Let us first focus on constructing the data source snapshot. As explained earlier, we are using HeaderItem
as the section identifier type, meaning 1 HeaderItem
instance will represent 1 collection view section. Therefore, to construct a data source snapshot with 3 sections is fairly simple, just append modelObjects
as sections will do.
var dataSourceSnapshot = NSDiffableDataSourceSnapshot<HeaderItem, ListItem>()
// Create collection view section based on number of HeaderItem in modelObjects
dataSourceSnapshot.appendSections(modelObjects)
dataSource.apply(dataSourceSnapshot)
After that, let’s construct the required section snapshots. Here’s how we do it:
// Loop through each header item so that we can create a section snapshot for each respective header item.
for headerItem in modelObjects {
// 1
// Create a section snapshot
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
// 2
// Create a header ListItem & append as parent
let headerListItem = ListItem.header(headerItem)
sectionSnapshot.append([headerListItem])
// 3
// Create an array of symbol ListItem & append as child of headerListItem
let symbolListItemArray = headerItem.symbols.map { ListItem.symbol($0) }
sectionSnapshot.append(symbolListItemArray, to: headerListItem)
// 4
// Expand this section by default
sectionSnapshot.expand([headerListItem])
// 5
// Apply section snapshot to the respective collection view section
dataSource.apply(sectionSnapshot, to: headerItem, animatingDifferences: false)
}
Let’s go through the above code in details:
- For each
headerItem
which representing a collection view section, create a section snapshot withListItem
as its item identifier type. - Create a
ListItem
withheaderItem
as associated value and append it to the section snapshot. This will create a section in the section snapshot. - Convert
headerItem
‘ssymbols
array into an array ofListItem
and append it to the section represented byheaderListItem
. - The section represented by
headerListItem
should be expanded by default. - Apply the section snapshot to the collection view section represented by
headerItem
.
That’s about it! You have successfully created an expandable list using a multi-section collection view. Build and run your sample code to see it in action.
List with Expandable Sections
All the while we are focusing on making a cell (header cell) that expands / collapses when being tapped. But what if you want to have a list with expandable sections as shown below?
Lucky for us, Apple has made achieving this kind of layout extremely easy. All we need to do is to set the collection view layout’s header mode to .firstItemInSection
.
var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
layoutConfig.headerMode = .firstItemInSection
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
By setting the header mode to .firstItemInSection
, the first UICollectionViewListCell
within a collection view section will automatically use a header appearance.
Wrapping Up
There are 3 takeaways from this article:
- Section identifier type (
HeaderItem
) is only used for collection view section representation. It has nothing to do with the data being shown on the collection view. It also has nothing to do with the expandable section. - The expand/collapse behavior of a collection view is mainly controlled by the structure of an
NSDiffableDataSourceSnapshot
. It has nothing to do with the section identifier type (HeaderItem
). - To create a list with expandable sections, just set the collection view header mode to
.firstItemInSection
will do.
All this might seem a bit confusing at first. But once you understand the concept behind, building an expandable list is just a matter of constructing the required snapshots correctly.
You can find the full sample code of part 1 and part 2 on GitHub.
Do you find this article helpful? Let me know in the comment section below. You can also reach out to me at Twitter and subscribe to my monthly newsletter.
Thanks for reading. 👨🏻💻
Further Readings
- UICollectionView List with Custom Cell and Custom Configuration
- Designing Custom UICollectionViewListCell in Interface Builder
- Declarative UICollectionView List Header and Footer
- UICollectionView List with Interactive Custom Header
- Replicate the Expandable Date Picker Using UICollectionView List
👋🏻 Hey!
While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.
- ✨ Bartender: Superpower your menu bar and take full control over your menu bar items.
- ✨ CleanShot X: The best screen capture app I’ve ever used.
- ✨ PixelSnap: Measure on-screen elements with ease and precision.
- ✨ iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.