[SC]()

iOS. Apple. Indies. Plus Things.

Collapsible UICollectionView Sections

// Written by Jordan Morgan // Apr 23rd, 2021 // Read it in about 2 minutes // RE: Snips

This post is brought to you by Emerge Tools, the best way to build on mobile.

let concatenatedThoughts = """

Welcome to Snips! Here, you'll find a short, fully code complete sample with two parts. The first is the entire code sample which you can copy and paste right into Xcode. The second is a step by step explanation. Enjoy!

"""

The Scenario

Create a basic UICollectionView with a collapsing section.

import UIKit

// 1
struct Item: Hashable {
    let id = UUID()
    let title: String
    let subItems: [Item]
}

// 2
typealias CollectionDataSource = UICollectionViewDiffableDataSource<Int, Item>

class CollapseViewController: UIViewController {
    
    private let data: [Item] = {
        return [Item(title: "Programming Languages", subItems: [Item(title: "Swift", subItems: []),
                                                                Item(title: "C++", subItems: []),
                                                                Item(title: "C#", subItems: [])])]
    }()
    
    private lazy var collectionView: UICollectionView = {
        let listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        collectionView.backgroundColor = .systemGroupedBackground
        return collectionView
    }()
    
    lazy var datasource: CollectionDataSource = {
        // 3
        let parentItemCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, indexPath, model in
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = model.title
            contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline)
            cell.contentConfiguration = contentConfiguration
            
            let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header)
            cell.accessories = [.outlineDisclosure(options: disclosureOptions)]
        }
        
        let subItemsCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, indexPath, model in
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = model.title
            contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .subheadline)
            contentConfiguration.textProperties.color = .label
            cell.contentConfiguration = contentConfiguration
            
            let disclosureOptions = UICellAccessory.disclosureIndicator()
            cell.accessories = [disclosureOptions]
        }
        
        let datasource = CollectionDataSource(collectionView: collectionView) { collectionView, indexPath, model in
            // 4
            let configType = model.subItems.isEmpty ? subItemsCellRegistration : parentItemCellRegistration
            
            return collectionView.dequeueConfiguredReusableCell(using: configType,
                                                                for: indexPath,
                                                                item: model)
        }
            
        return datasource
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        
        // 5
        let firstParentItem = data.first!
        let subItems = data.first!.subItems
        
        // Create the datasource
        var snapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        
        // Append the first section
        snapshot.append([firstParentItem], to: nil)
        
        // Populate that section
        snapshot.append(subItems, to: firstParentItem)
        
        // Apply
        datasource.apply(snapshot, to: 0)
    }
}

Which results in:

Collapsing Sections

The Breakdown


Step 1
Create a data model that supports a parent-child relationship.


Step 2
This is a stylistic choice, but I typically typelias any diffable datasource for readability.


Step 3
Using modern cell registration APIs, we create two configurations:

  1. One for any top-level item
  2. One for any of the top-level item’s children


Step 4
Creating our diffable datasource, we check the model to determine if it’s a parent or a child. Depending on the result, we’ll return the correct cell configuration and create the cell.


Step 5
Finally, we use a section snapshot, NSDiffableDataSourceSectionSnapshot, and use its new function to append either a top-level item or children. If the parent parameter is nil, it’s treated as a top level item. If you do supply a parent, then those items will be inserted into that section.

Until next time ✌️

···

Spot an issue, anything to add?

Reach Out.