Building an Expandable List Using UICollectionView: Part 1
source link: https://swiftsenpai.com/development/collectionview-expandable-list-part1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Building an Expandable List Using UICollectionView: Part 1
Expandable list or some might call it expandable table view, is a very common UI design that is used in a large variety of apps. Despite the fact that it is so popular, UIKit does not have any native APIs that support this kind of layout.
With the introduction of iOS 14, this is no longer true!
According to Apple, developers can now easily create an expandable list by using the new UICollectionView
list layout APIs. Unfortunately, after checking out the sample code provided by Apple, I found that it is extremely confusing and very difficult to understand.
Therefore, I would like to take a shot at writing an article that clearly explains how you can effectively build an expandable list using a UICollectionView
.
This is the first part of a 2 parts blog post. In the first part, we will be focusing on building an expandable list within a single-section collection view. In the second part, we will attempt to modify the list we build in part 1 to display the same model objects in a multi-section collection view. By the end of part 2, you should be able to clearly understand the relationship between each component involved in creating an expandable list.
Before proceeding, make sure to go through my previous article “Building a List with UICollectionView in Swift” if you are not familiar with the basic concept of UICollectionView
list.
With all the being said, let’s begin.
The Sample App
The following animated GIF showcases the sample app that we are going to build.
As shown, the collection view consists of 1 section. Within the section, there are header cells and symbol cells that are in charge of showing different SFSymbol
s being grouped based on category.
Defining Required Data Types
Before we start building the expandable list, we must first define all the required data types.
Let’s start by defining the collection view section.
enum Section {
case main
}
After that, let’s define the data type that will hold the header and symbol cell’s data.
// 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)!
}
}
Next up, we will define an enum named ListItem
. This enum type will act as the collection view’s data source item identifier type.
enum ListItem: Hashable {
case header(HeaderItem)
case symbol(SFSymbolItem)
}
At this point, you might wonder why we need to use the ListItem
as the data source item identifier type. I will explain that in detail once we start configuring the collection view’s data source.
Lastly, let’s define the sample model objects that will be consumed by the collection view. Do note that in most cases, the model objects will be provided by a backend server in the form of JSON data.
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"),
]),
]
With all the required data types in place, we are now ready to construct our expandable list.
Configuring Collection View
First thing first, let’s set up the collection view to take up the entire view controller’s view. As usual, we will use insetGrouped
as our list appearance.
// Set layout to collection view
let layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
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),
])
Performing Cell Registration
As mentioned earlier, our expandable list consists of 2 types of cells — header cell and symbol cell.
Even though both of them will be of type UICollectionViewListCell
, we still need to create 2 different CellRegistration
instances. This is because both of them have different item identifier types and different behavior.
The header cell registration will have HeaderItem
as it’s item identifier type. At the same time, within the cell registration handler, we must add an outline disclosure accessory to the header cell. This is to enable the expand / collapse behavior of the header cell.
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)]
}
On the other hand, the symbol cell registration will have SFSymbolItem
as its item identifier type.
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
}
With both header and symbol cell registration ready, we can proceed to configure the collection view data source.
Initializing Data Source
Still remember the ListItem
enum that we defined earlier? This is where it comes into action.
As you may already know, the UICollectionViewDiffableDataSource
only supports 1 item identifier type. However, in our expandable list, we have 2 types of data — HeaderItem
and SFSymbolItem
.
To solve this problem, we can leverage an enum with associated value to wrap both HeaderItem
and SFSymbolItem
into a single data type called ListItem
. With that, we will be able to initialize the UICollectionViewDiffableDataSource
using ListItem
as item identifier type.
The fact that ListItem
is an enum, we can now use a switch
statement within the cell provider closure to check for all the possible data types and dequeue a collection view cell accordingly.
dataSource = UICollectionViewDiffableDataSource<Section, ListItem>(collectionView: collectionView) {
(collectionView, indexPath, listItem) -> UICollectionViewCell? in
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
}
}
Do note that while dequeuing a reusable cell, you must always match the item’s data type with the respective cell registration item identifier type.
Matching item’s data type with cell registration item identifier typeSetting Up Snapshots
Understanding Different Kind of Snapshots
In order to display the model objects in the collection view in an expandable manner, we will have to make use of the NSDiffableDataSourceSnapshot
and NSDiffableDataSourceSectionSnapshot
.
NSDiffableDataSourceSnapshot
was introduced in iOS 13. Generally speaking, a NSDiffableDataSourceSnapshot
is used to provide data for collection views (or table views). We can use it to define the collection view sections, as well as the items within each collection view section.
On the other hand, NSDiffableDataSourceSectionSnapshot
was introduced in this year’s WWDC alongside iOS 14. It is used to provide data for a specific collection view (or table view) section. It functions similarly to NSDiffableDataSourceSnapshot
where we can use it to represent sections and items. Therefore, we can treat it as a mini NSDiffableDataSourceSnapshot
for the collection view (or table view) section.
To summarise:
- A collection view can only have one
NSDiffableDataSourceSnapshot
. - Within a collection view section can only have one
NSDiffableDataSourceSectionSnapshot
. - A collection view can have more than one
NSDiffableDataSourceSectionSnapshot
. - The
NSDiffableDataSourceSectionSnapshot
got nothing to do with the collection view section, it is used to define multi-section data within a collection view section. If you do not have multi-section data within a collection view section, then you do not need aNSDiffableDataSourceSnapshot
.
OK, enough with the theory. It’s time to construct the snapshots.
Constructing the Snapshots
For our sample app, we are trying to show multi-section data within a collection view section. Therefore, we will need to construct 1 NSDiffableDataSourceSnapshot
and 1 NSDiffableDataSourceSectionSnapshot
.
The following diagram illustrates the structure of the snapshots that we need to construct.
Structure of the snapshotsConstructing the NSDiffableDataSourceSnapshot
is fairly straightforward. Since the collection view only consists of 1 section, we just need to append the main
section to the snapshot, and that’s about it.
var dataSourceSnapshot = NSDiffableDataSourceSnapshot<Section, ListItem>()
// Create a section in the data source snapshot
dataSourceSnapshot.appendSections([.main])
dataSource.apply(dataSourceSnapshot)
Do note that the data source snapshot and the data source we initialized earlier must have the same section identifier type and item identifier type.
Next up, let’s construct the NSDiffableDataSourceSectionSnapshot
. Here’s how we do it:
// 1
// Create a section snapshot for main section
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
// 2
for headerItem in modelObjects {
// 3
// Create a header ListItem & append as parent
let headerListItem = ListItem.header(headerItem)
sectionSnapshot.append([headerListItem])
// 4
// Create an array of symbol ListItem & append as children of headerListItem
let symbolListItemArray = headerItem.symbols.map { ListItem.symbol($0) }
sectionSnapshot.append(symbolListItemArray, to: headerListItem)
// 5
// Expand this section by default
sectionSnapshot.expand([headerListItem])
}
// 6
// Apply section snapshot to main section
dataSource.apply(sectionSnapshot, to: .main, animatingDifferences: false)
Let’s go through the above code in details:
- Create a section snapshot with
ListItem
as its item identifier type. Note that the section snapshot item identifier type must match with the data source item identifier type. - Loop through each
HeaderItem
instance inmodelObjects
to construct a section snapshot that represents the same data hierarchy asmodelObjects
. - 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. - Display the data in the section snapshot by applying the section snapshot to the data source’s
main
section.
So there you have it! Build and run the sample code to see everything in action.
You can get the full sample code here.
Wrapping Up
This concludes the first part of “Building an Expandable List Using UICollectionView“. As you may have noticed, I purposely create the expandable list in a single-section collection view so that you will not mix up the collection view section and the NSDiffableDataSourceSectionSnapshot
section.
In the second part, we will modify the code we have written so far to create an expandable list using a multi-section collection view. Here’s a sneak peek of what we trying to build in part 2:
Stay tuned!
If you like this article, feel free to follow me on Twitter, and subscribe to my monthly newsletter.
Thanks for reading.
[Updated: 22 September 2020]
You can find part 2 of this article here:
Building an Expandable List Using UICollectionView: Part 2
Further Readings
Recommend
-
16
One of the most expected features of SwiftUI 2.0 is a SwiftUI alternative to UICollectionView . UICollectionView provides us an easy way to build super custom interfaces like calendar...
-
21
In my previous article, we had discussed how to add a declarative header and footer to the UICollectionView List. This week, let’s take one step further and talk about how you can add a custom header to your collection view decla...
-
23
Declarative UICollectionView List Header and Footer If you have been following my work for the past couple of months, you have probably read my articles about constructing a
-
16
Building an Expandable List Using UICollectionView: Part 2 In last week’s article, we learned how to leverage the NSDiffableDataSourceSecti...
-
11
UICollectionView List with Custom Cell and Custom Configuration In iOS14, Apple introduced the list layout in UICollectionView, allowing developers to easily create a list using
-
4
Building a List with UICollectionView in Swift Since the introduction of UICollectionView in iOS6, UICollectionView has been the default component to go to when it comes to building a grid layout. In WWDC 20...
-
24
Replicate the Expandable Date Picker Using UICollectionView List The expandable date picker is a very common UI component in the Apple design system. You can find it in the iOS Reminder and Calendar App. Even though it does not come...
-
10
UICollectionView with UIContentConfiguration for Cells, Compositional List Layout and Diffable Datasource. // Written by
-
5
Last updated on: August 27, 2022 In this tutorial, I’ll show you how to make an expandable list using Jetpack Compose. Each row contains two composable views: a header (HeaderView
-
9
SwiftUI Build Dynamic And Expandable SwiftUI List With DisclosureGroup In the
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK