You are currently viewing How to Reload the Diffable Section Header

How to Reload the Diffable Section Header

A couple of months ago, I have wrote an article about “The Modern Ways to Reload Your Table and Collection View Cells“. The concept is simple, we first identify the desired data source snapshot item, update it, and then apply the changes to the data source snapshot.

When It comes to reloading an expandable list header, things get a little bit trickier. This is because we are now dealing with a diffable data source section snapshot, which is another layer deeper into the data source snapshot.

In this article, I would like to share with you the approach I use to effectively reload an expandable list header.


The Sample App

The best way to showcase the approach is by using a sample app.

How to reload the expandable section header in the diffable data source
The sample app

In our sample app, we will use the diffable data source to construct an expandable list consisting of 2 sections. Every time when a cell is selected, its section header title will be updated accordingly.


Getting Ready

As usual, we must first define the section and item identifier for our list. For better code readability, let’s define a Child struct to hold the cell’s title and a Parent struct to hold the section’s title and all its children. With both Parent and Child struct in place, let’s construct our sample app’s model objects as well.

// MARK: Identifier Types
struct Parent: Hashable {
    var title: String
    let children: [Child]
}

struct Child: Hashable {
    let title: String
}

// MARK: Model objects
let parents = [
    
    Parent(title: "Parent 1", children: [
        Child(title: "Child 1 - A"),
        Child(title: "Child 1 - B"),
        Child(title: "Child 1 - C"),
    ]),
    
    Parent(title: "Parent 2", children: [
        Child(title: "Child 2 - A"),
        Child(title: "Child 2 - B"),
        Child(title: "Child 2 - C"),
        Child(title: "Child 2 - D"),
    ]),
]

In order to use both Parent and Child type in our data source, we then need to define a data type wrapper as the data source’s item identifier type. Let’s name it DataItem.

enum DataItem: Hashable {
    case parent(Parent)
    case child(Child)
}

var dataSource: UICollectionViewDiffableDataSource<Parent, DataItem>!
var collectionView: UICollectionView!

With all that in place, we can now proceed to construct the expandable list.


Building the Expandable List

If you have read my previous article about how to build an expandable list, you should feel very familiar with the following set of codes. If you haven’t, I highly encourage you to check it out before proceeding.

In short, our ultimate goal for this section is to convert the model objects we created earlier into a data source snapshot with the following structure:

The diffable data source section snapshot structure
The section snapshot structure
override func viewDidLoad() {
    super.viewDidLoad()

    // MARK: Create list layout
    var layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    layoutConfig.headerMode = .firstItemInSection
    let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)

    // MARK: Configure collection view
    collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: listLayout)
    collectionView.delegate = self
    view.addSubview(collectionView)
    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, Parent> {
        (cell, indexPath, parent) in
        
        // Set parent's title to header cell
        var content = cell.defaultContentConfiguration()
        content.text = parent.title
        cell.contentConfiguration = content
        
        let headerDisclosureOption = UICellAccessory.OutlineDisclosureOptions(style: .header)
        cell.accessories = [.outlineDisclosure(options:headerDisclosureOption)]
    }

    let childCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Child> {
        (cell, indexPath, child) in
        
        // Set child title to cell
        var content = cell.defaultContentConfiguration()
        content.text = child.title
        cell.contentConfiguration = content
    }
    
    // MARK: Initialize data source
    dataSource = UICollectionViewDiffableDataSource<Parent, DataItem>(collectionView: collectionView) {
        (collectionView, indexPath, listItem) -> UICollectionViewCell? in
        
        switch listItem {
        case .parent(let parent):
        
            // Dequeue header cell
            let cell = collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration,
                                                                    for: indexPath,
                                                                    item: parent)
            return cell
        
        case .child(let child):
            
            // Dequeue cell
            let cell = collectionView.dequeueConfiguredReusableCell(using: childCellRegistration,
                                                                    for: indexPath,
                                                                    item: child)
            return cell
        }
    }
    
    // Loop through each parent items to create a section snapshots.
    for parent in parents {
        
        // Create a section snapshot
        var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<DataItem>()
        
        // Create a parent DataItem & append as parent
        let parentDataItem = DataItem.parent(parent)
        sectionSnapshot.append([parentDataItem])
        
        // Create an array of child items & append as children of parentDataItem
        let childDataItemArray = parent.children.map { DataItem.child($0) }
        sectionSnapshot.append(childDataItemArray, to: parentDataItem)
        
        // Expand this section by default
        sectionSnapshot.expand([parentDataItem])
        
        // Apply section snapshot to the respective collection view section
        dataSource.apply(sectionSnapshot, to: parent, animatingDifferences: false)
    }
}

With the above code in place, you can now run the sample app and see an expandable list being displayed on the screen. If you try to tap on the cells, nothing will happen to the section header title. Don’t worry, we will work on that next.


Let’s Reload the Header

As you might have expected, we will reload the section header in the collectionView(collectionView:didSelectItemAt:) delegate method.

As mentioned earlier, we are using DataItem as our item identifier type, which is a value type. Therefore, we can reload the section header using a strategy similar to reloading a cell with value type data, instead of changing the data source snapshot, this time we will have to manipulate the section snapshot.

Here’s how:

func collectionView(_ collectionView: UICollectionView,
                    didSelectItemAt indexPath: IndexPath) {
    
    let selectedSection = dataSource.snapshot().sectionIdentifiers[indexPath.section]
    let selectedSectionSnapshot = dataSource.snapshot(for: selectedSection)
    
    // 1
    // Obtain a reference to the selected parent
    guard
        let selectedParentDataItem = selectedSectionSnapshot.rootItems.first,
        case let DataItem.parent(selectedParent) = selectedParentDataItem else {
        return
    }
    
    // 2
    // Obtain a reference to the selected child
    let selectedChildDataItem = selectedSectionSnapshot.items[indexPath.item]
    guard case let DataItem.child(selectedChild) = selectedChildDataItem else {
        return
    }
    
    // 3
    // Create a new parent with selectedChild's title
    let newParent = Parent(title: selectedChild.title, children: selectedParent.children)
    let newParentDataItem = DataItem.parent(newParent)
    
    // 4
    // Avoid crash when user tap on same cell twice (snapshot data must be unique)
    guard selectedParent != newParent else {
        return
    }

    // 5
    // Replace the parent in section snapshot (by insert new item and then delete old item)
    var newSectionSnapshot = selectedSectionSnapshot
    newSectionSnapshot.insert([newParentDataItem], before: selectedParentDataItem)
    newSectionSnapshot.delete([selectedParentDataItem])
    
    // 6
    // Reconstruct section snapshot by appending children to `newParentDataItem`
    let allChildDataItems = selectedParent.children.map { DataItem.child($0) }
    newSectionSnapshot.append(allChildDataItems, to: newParentDataItem)
    
    // 7
    // Expand the section
    newSectionSnapshot.expand([newParentDataItem])
    
    // 8
    // Apply new section snapshot to selected section
    dataSource.apply(newSectionSnapshot, to: selectedSection, animatingDifferences: true) {
        // The cell's select state will be gone after applying a new snapshot, thus manually reselecting the cell.
        collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .left)
    }
}

Let’s break down the above code in details:

  1. Obtain a reference to the parent of the selected child. This is needed later on when we want to create a new parent with an updated title.
  2. Obtain a reference to the selected child so that we know what is the title of the new parent.
  3. Create a new parent that has the same children as the selected parent but with an updated title (selected child’s title).
  4. This part is important to avoid the NSInternalInconsistencyException! As we all know, all items in the data source snapshot must be unique. If users tap on the same cell twice, then selectedParent (already in data source snapshot) and newParent will be the same. When this happens, we should not proceed to insert the newParent into the data source snapshot.
  5. Create a new copy of the selected section snapshot and replace the selected parent item with the new parent item we created earlier.
  6. When we replaced the selected parent item, we removed the entire section snapshot data hierarchy altogether. Thus we must reconstruct the section snapshot by re-appending all the child items to the new parent item.
  7. A section snapshot section is collapsed by default, therefore we need to manually expand the section represented by the new parent item.
  8. Lastly, apply the new section snapshot to the selected section, then we are done!

That’s about it, build and run your code and see everything comes together. You can get the full sample code of this article here.


Dealing with Reference Type

At this point, you might be wondering, what happens if both Parent and Child are reference type? Will the sample code still work correctly?

The answer is yes.

This might sound unrealistic at first, but if you look closely, the way we update the section title is by creating a new parent object with the desired title and set it to the section snapshot. Therefore, it doesn’t matter Parent is a class or struct, the way to create a new object is still the same.

Furthermore, since we have created a data type wrapper (DataItem) to wrap around both Parent and Child type, changing both of these types to reference type will not affect the code where we update the section snapshot. This is because as long as we are still using DataItem as an item identifier type, everything will still work correctly.


Wrapping Up

There you have it! Reloading a section header when using a diffable data source might seem overly complicated at first. But once you understand the concept behind and how it works under the hood, it is just a matter of replacing the old section snapshot with a new one.

Feel free to follow me on Twitter, and subscribe to my monthly newsletter for more articles related to iOS development.


Further Readings


👋🏻 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.