18

Declarative UICollectionView List Header and Footer

 3 years ago
source link: https://swiftsenpai.com/development/declarative-list-header-footer/
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.

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 basic and expandable list declaratively using cell registration, content configuration, and data source snapshot.

In this article, let’s take one step further and learn how you can construct a list with header and footer declaratively without the need of using storyboard or interface builder.

For simplicity’s sake, this article will only be focusing on configuring and adding a system default header and footer, ways to add a custom header and footer will be a story for another day.


The Sample App

Declarative UICollectionView List Header and Footer
The sample app

We are going to create a sample app that is similar to what we have in “Building an Expandable List Using UICollectionView: Part 2” but without the expand/collapse functionality.

Furthermore, we will show the symbol count for each section in the footer. On top of that, we will also perform some minor configuration on the header to make it a bit more eye-catching.

Note:

Concepts in this article such as item and section identifier, list layout configuration, cell registration and, diffable data source snapshot have been discussed in detail in this, this and this article. Feel free to refer to them when you have any questions related to those concepts.


The Item and Section Identifier Type

As usual, before constructing the list, we must first define the item and section identifier type.

For item identifier type, we will use the SFSymbolItem struct as shown below.

struct SFSymbolItem: Hashable {
    let name: String
    let image: UIImage
    
    init(name: String) {
        self.name = name
        self.image = UIImage(systemName: name)!
    }
}

For section identifier type, we will use the HeaderItem struct as shown below.

struct HeaderItem: Hashable {
    let title: String
    let symbols: [SFSymbolItem]
}

After defining the item and section identifier type, we can now define the sample model objects that will be consumed by the collection view.

let modelObjects = [
    
    HeaderItem(title: "Devices", symbols: [
        SFSymbolItem(name: "iphone.homebutton"),
        SFSymbolItem(name: "pc"),
        SFSymbolItem(name: "headphones"),
    ]),
    
    HeaderItem(title: "Weather", symbols: [
        SFSymbolItem(name: "sun.min"),
        SFSymbolItem(name: "sunset.fill"),
    ]),
    
    HeaderItem(title: "Nature", symbols: [
        SFSymbolItem(name: "drop.fill"),
        SFSymbolItem(name: "flame"),
        SFSymbolItem(name: "bolt.circle.fill"),
        SFSymbolItem(name: "tortoise.fill"),
    ]),
]

With all these in place, it is time to construct the list.


The Initial Setup

In order for us to start declaring the header and footer. We must first configure the collection view, after that, perform cell registration and then initialize the diffable data source.

// MARK: Create list layout
var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
// 1
layoutConfig.headerMode = .supplementary
layoutConfig.footerMode = .supplementary
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
// 2
let symbolCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SFSymbolItem> {
    (cell, indexPath, symbolItem) in
    
    // Configure cell content
    var configuration = cell.defaultContentConfiguration()
    configuration.image = symbolItem.image
    configuration.text = symbolItem.name
    cell.contentConfiguration = configuration
}

// MARK: Initialize data source
// 3
dataSource = UICollectionViewDiffableDataSource<HeaderItem, SFSymbolItem>(collectionView: collectionView) {
    (collectionView, indexPath, symbolItem) -> UICollectionViewCell? in
    
    // 4
    // Dequeue symbol cell
    let cell = collectionView.dequeueConfiguredReusableCell(using: symbolCellRegistration,
                                                            for: indexPath,
                                                            item: symbolItem)
    return cell
}

Wow, this is quite a lot of code!

Don’t worry, the above code is very similar to what we have in the “Building an Expandable List Using UICollectionView: Part 2” sample app. However, there are 4 significant differences:

  1. The collection view must have both header and footer mode set to .supplementary in order to trigger the data source’s supplementary view provider (more on that later).
  2. Use SFSymbolItem as item identifier type when performing cell registration.
  3. Use SFSymbolItem as item identifier type when initializing the diffable data source.
  4. Our sample app only has 1 type of cell, therefore within the data source’s cell provider closure, we only need to dequeue 1 type of cell.

Setting Up Supplementary Views

After completing the initial setup, we are now ready to work on the header and footer (a.k.a supplementary view). In order to display the supplementary views, there are 2 things that need to be done:

  1. Perform supplementary view registration
  2. Define supplementary view provider

Perform Supplementary View Registration

The way to register a supplementary view is very similar to how we register a cell. For the sample app, we need to register 2 types of supplementary view: header and footer.

Let us first focus on header registration. Here’s how we will do it:

let headerRegistration = UICollectionView.SupplementaryRegistration
<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) {
    [unowned self] (headerView, elementKind, indexPath) in
    
    // Obtain header item using index path
    let headerItem = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
    
    // Configure header view content based on headerItem
    var configuration = headerView.defaultContentConfiguration()
    configuration.text = headerItem.title
    
    // Customize header appearance to make it more eye-catching
    configuration.textProperties.font = .boldSystemFont(ofSize: 16)
    configuration.textProperties.color = .systemBlue
    configuration.directionalLayoutMargins = .init(top: 20.0, leading: 0.0, bottom: 10.0, trailing: 0.0)
    
    // Apply the configuration to header view
    headerView.contentConfiguration = configuration
}

The above code consist of 3 major parts:

  1. The supplementary view subclass use for the registration
  2. The element kind of the supplementary view
  3. The supplementary view handler
Supplementary view registration in UICollectionView
Supplementary view registration

As you can see, we are using UICollectionViewListCell as the supplementary view (header view) subclass. This might seem a bit weird at first, but according to Apple documentation, UICollectionViewListCell usage is not only limited to cell creation, it can be used as a subclass of header and footer view as well.

Another thing to take note of is that we must specify the element kind of the supplementary view. Make sure to use the constant value defined in UIKit (UICollectionView.elementKindSectionHeader) so that when calling headerView.defaultContentConfiguration() in the supplementary view handler, we will be able to get the correct system default header configuration object.

With that, we can apply the exact same concept to register a footer view.

let footerRegistration = UICollectionView.SupplementaryRegistration
<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) {
    [unowned self] (footerView, elementKind, indexPath) in
    
    let headerItem = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
    let symbolCount = headerItem.symbols.count
    
    // Configure footer view content
    var configuration = footerView.defaultContentConfiguration()
    configuration.text = "Symbol count: \(symbolCount)"
    footerView.contentConfiguration = configuration
}

Define Supplementary View Provider

The data source’s supplementary view provider is a closure where you specify which supplementary view should be used for header or footer. Here’s the implementation:

dataSource.supplementaryViewProvider = { [unowned self]
    (collectionView, elementKind, indexPath) -> UICollectionReusableView? in
    
    if elementKind == UICollectionView.elementKindSectionHeader {
        
        // Dequeue header view
        return self.collectionView.dequeueConfiguredReusableSupplementary(
            using: headerRegistration, for: indexPath)
        
    } else {
        
        // Dequeue footer view
        return self.collectionView.dequeueConfiguredReusableSupplementary(
            using: footerRegistration, for: indexPath)
    }
}

Note how you need to dequeue the supplementary view based on the element kind using the header and footer registration object we defined previously.

Also note that because we have set the collection view header and footer mode to .supplementary earlier on, this makes defining the supplementary view provider a compulsory step. Or else you will get an NSInternalInconsistencyException as shown below:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: self.supplementaryViewProvider || (self.supplementaryReuseIdentifierProvider && self.supplementaryViewConfigurationHandler)'

Constructing Data Source Snapshot

The last part of the puzzle is to construct a data source snapshot and put all of our hard work together.

var dataSourceSnapshot = NSDiffableDataSourceSnapshot<HeaderItem, SFSymbolItem>()

// Create collection view section based on number of HeaderItem in modelObjects
dataSourceSnapshot.appendSections(modelObjects)

// Loop through each header item to append symbols to their respective section
for headerItem in modelObjects {
    dataSourceSnapshot.appendItems(headerItem.symbols, toSection: headerItem)
}

dataSource.apply(dataSourceSnapshot)

The logic above is pretty straightforward. We first construct all possible sections by appending all HeaderItem instance as the snapshot section. After that, we loop through each HeaderItem instances and append its symbol array to the respective section.

That’s it! Now hit the run button and see everything comes together.


Expandable vs. Non-expandable List

At this point, some of you that have read my previous article “Building an Expandable List Using UICollectionView: Part 2” might be wondering why we are not using the expandable list approach (involving .firstItemInSection header mode, ListItem, and NSDiffableDataSourceSectionSnapshot) to construct the list?

You can definitely use the expandable list approach to achieve what we are trying to achieve in this article. However, your code will be much more complex than what we currently have, especially at the part where you construct the data source snapshot. This is because you will need to deal with ListItem and NSDiffableDataSourceSectionSnapshot.

Feel free to get the sample code at Github to see the differences between each approach.


Wrapping Up

I really like the declarative way of constructing a list, I encourage everyone to give it a try. Here are a few of its benefits:

  • It does not involve storyboard or interface builder. Thus merge conflicts will not be a huge problem.
  • It is easy to use and easy to maintain.
  • It has better separation of concerns.

I hope you find this article helpful. Feel free to reach out to me on Twitter if you have any questions, thoughts or comments. 

Thanks for reading. 👨🏻‍💻


Future Readings



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK