7

I have looked at this question, which is similar: How to deal with empty items section in UICollectionView CompositionalLayout, but the answer there seems to be either leave sections out in the snapshot (which I do, but that leaves another problem, which I describe later) or to render a very small section. That solution does not seem like a good solution.


I have a collection view using a compositional layout with a diffable data source. The collection view has four sections, but each of those sections is optional meaning that if the corresponding data for that section is empty, then the section should not be displayed.

Code

Layout Definition

I have a section provider that uses the sectionIndex to configure what each section should look like. I think this is bad because then if I do not have data for section three in the snapshot, for instance, then everything that should normally be in section four will now have an indexPath that will cause it to be laid out like section three.

And each section has different item sizes and some are orthogonal scrolling sections. So if section four data is rendered using the section three layout, then it will look wrong.

NSCollectionLayoutSection * _Nullable (^sectionProvider)(NSInteger, id<NSCollectionLayoutEnvironment> _Nonnull) = ^NSCollectionLayoutSection * _Nullable (NSInteger sectionIndex, id<NSCollectionLayoutEnvironment> _Nonnull layoutEnvironment) {
    if (sectionIndex == 0) {
        //configure and return a layout for the first section
    } else if (sectionIndex == 1) {
        //configure and return a layout for the second section
    } else if (sectionIndex == 2) {
        //configure and return a layout for the third section
    } else if (sectionIndex == 3) {
        //configure and return a layout for the fourth section
    }
    return nil;
};


UICollectionViewCompositionalLayoutConfiguration *configuration = [[UICollectionViewCompositionalLayoutConfiguration alloc] init];
configuration.interSectionSpacing = 10;
configuration.scrollDirection = UICollectionViewScrollDirectionVertical;


self->_collectionViewLayout = [[UICollectionViewCompositionalLayout alloc] initWithSectionProvider:sectionProvider configuration:configuration];

Data Source Definition

This is where the data source is defined. Each section uses a different data model class, so I decide which type of cell to use based on the type of the data model class, not on the index path.

self->_dataSource = [[UICollectionViewDiffableDataSource alloc] initWithCollectionView:self.collectionView cellProvider:^UICollectionViewCell * _Nullable(UICollectionView * _Nonnull collectionView, NSIndexPath * _Nonnull indexPath, id  _Nonnull item) {
    if ([item isKindOfClass:[MyFirstSectionModel class]]) {
        return [collectionView dequeueConfiguredReusableCellWithRegistration:firstSectionCellRegistration forIndexPath:indexPath item:item];
    } else if ([item isKindOfClass:[MySecondSectionModel class]]) {
        return [collectionView dequeueConfiguredReusableCellWithRegistration:secondSectionCellRegistration forIndexPath:indexPath item:item];
    } else if ([item isKindOfClass:[MyThirdSectionModel class]]) {
        return [collectionView dequeueConfiguredReusableCellWithRegistration:thirdSectionCellRegistration forIndexPath:indexPath item:item];
    } else if ([item isKindOfClass:[MyFourthSectionModel class]]) {
        return [collectionView dequeueConfiguredReusableCellWithRegistration:fourthSectionCellRegistration forIndexPath:indexPath item:item];
    }
    return nil;
}];

Snapshot Construction

Here is where each section is either included (if it has data) or excluded (if the section is empty). But leaving a section out (like for example, if section three does not have any data, then it will be left out, but then that will make section four's data to have an index path with an index of 2, which will not work with the section provider.

If I insert an empty section into the snapshot, that still will not work because some of these sections have headers, so if it is a section that has a header then the header will still be displayed. But even if none of the sections had headers, I think it would still render some extra amount of empty space for the section (but this may be incorrect).

- (void)reloadDataSourceAnimated:(BOOL)animated {
    NSDiffableDataSourceSnapshot<CICustomerReviewsSectionIdentifierType, CICustomerReviewsItemIdentifierType> *snapshot = [[NSDiffableDataSourceSnapshot alloc] init];
    
    
    if (self.firstSectionItems.count) {
        [snapshot appendSectionsWithIdentifiers:@[MyFirstSectionIdentifier]];
        [snapshot appendItemsWithIdentifiers:@[self.firstSectionItems] intoSectionWithIdentifier:MyFirstSectionIdentifier];
    }
    
    if (self.secondSectionItems.count) {
        [snapshot appendSectionsWithIdentifiers:@[MySecondSectionIdentifier]];
        [snapshot appendItemsWithIdentifiers:@[self.secondSectionItems] intoSectionWithIdentifier:MySecondSectionIdentifier];
    }
    
    if (self.thirdSectionItems.count) {
        [snapshot appendSectionsWithIdentifiers:@[MyThirdSectionIdentifier]];
        [snapshot appendItemsWithIdentifiers:@[self.thirdSectionItems] intoSectionWithIdentifier:MyThirdSectionIdentifier];
    }
    
    if (self.fourthSectionItems.count) {
        [snapshot appendSectionsWithIdentifiers:@[MyFourthSectionIdentifier]];
        [snapshot appendItemsWithIdentifiers:self.fourthSectionItems intoSectionWithIdentifier:MyFourthSectionIdentifier];
    }
    
    
    [self.dataSource applySnapshot:snapshot animatingDifferences:animated];
}

Summary

So the problem is that if one or more of my sections does not have data, then when they get left out of the snapshot, that will cause the data for subsequent sections to be rendered in the wrong section (because the section provider configures sections based on the index and the indexPaths of each of the sections after the empty section(s) are no longer the original indexPath).

Question

  1. Is there a way to have the sections be optional and for any regular views and supplementary views to not be rendered for an "empty" section?
ashipma
  • 423
  • 5
  • 15

2 Answers2

2

I solved this problem by assigning my collection view data to local variable before applying datasource snaphot. This variable can be accessed by UICollectionViewCompositionalLayoutSectionProvider closure for determine which layout needs to be returned for a given index.

Example

Lets take this data model:

struct ViewControllerData {
    let texts: [String]
    let colors: [UIColor]
    let numbers: [Int]
}

Collection view datasource definition:

enum Section: Hashable {
    case first
    case second
    case third
}

enum SectionData: Hashable {
    case text(String)
    case color(UIColor)
    case number(Int)
}

lazy var datasource: UICollectionViewDiffableDataSource<Section, SectionData> = {
    
    let dataSource = UICollectionViewDiffableDataSource<Section, SectionData>(collectionView: self.collectionView) { [weak self] (collectionView, indexPath, data) -> UICollectionViewCell? in
        
        switch data {
        case .text(let text):
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TextCollectionViewCell.reuseIdentifier, for: indexPath) as? TextCollectionViewCell
            cell?.textLabel.text = text
            return cell
            
        case .color(let color):
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ColorCollectionViewCell.reuseIdentifier, for: indexPath) as? ColorCollectionViewCell
            cell?.colorView.backgroundColor = color
            return cell
            
        case .number(let number):
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NumberCollectionViewCell.reuseIdentifier, for: indexPath) as? NumberCollectionViewCell
            cell?.numberLabel.text = "\(number)"
            return cell
        }
    }
    
    dataSource.supplementaryViewProvider = ...

    return dataSource
}()

Configure diffable snapshot excluding sections which has no data and assign model data to a local variable:

private var currentData: ViewControllerData?

public func showData(_ data: ViewControllerData) {
    
    self.currentData = data
    
    var snapshot = NSDiffableDataSourceSnapshot<Section, SectionData>()
    
    if !data.texts.isEmpty {
        snapshot.appendSections([.first])
        snapshot.appendItems(data.texts.map { SectionData.text($0 )}, toSection: .first)
    }
    
    if !data.colors.isEmpty {
        snapshot.appendSections([.second])
        snapshot.appendItems(data.colors.map { SectionData.color($0) }, toSection: .second)
    }
    
    if !data.numbers.isEmpty {
        snapshot.appendSections([.third])
        snapshot.appendItems(data.numbers.map { SectionData.number($0) }, toSection: .third)
    }
    
    datasource.apply(snapshot, animatingDifferences: true)
}

Use this variable to provide correct section layout:

lazy var collectionViewLayout: UICollectionViewLayout = {
    
    let layout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
        
        guard let section = self?.currentData?.visibleSection(at: sectionIndex) else { return nil }
        
        switch section {
        case .first:
            let section = ...
            return section

        case .second:
            let header = ...
            let section = ...
            section.boundarySupplementaryItems = [header]
            return section
            
        case .third:
            let section = ...
            return section
        }
    }
    
    return layout
}()

visibleSection(at index:) is extension of ViewControllerData for convenience:

extension ViewControllerData {
    
    var visibleSections: [ViewController.Section] {

        var sections: [ViewController.Section] = []
        if !texts.isEmpty { sections.append(.first) }
        if !colors.isEmpty { sections.append(.second) }
        if !numbers.isEmpty { sections.append(.third) }
        
        return sections
    }
    
    func visibleSection(at index: Int) -> ViewController.Section? {
        guard visibleSections.indices.contains(index) else { return nil }
        return visibleSections[index]
    }
}

This variable can also be used in collection view data source for provide supplementary views:

dataSource.supplementaryViewProvider = { [weak self] (collectionView, kind, indexPath) in
    
    guard let section = self?.currentData?.visibleSection(at: indexPath.section) else { return nil }
    
    switch section {
    case .second:
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HeaderView.reuseIdentifier, for: indexPath) as? HeaderView
        header?.textLabel.text = "Colors section header"
        return header

    default: return nil
    }
}

Result:

Collection view without

ifau
  • 2,100
  • 1
  • 21
  • 29
  • 1
    I don't understand how this is so complicated to do, in a scenario that happens all the time. Compositional layout and snapshots should handle this easily by associating the layout to the object type. – christostsang Aug 11 '22 at 11:47
1

When you create your layout ask your datasource what section identifiers are currently present:

enum Section: Int {
    case sectionOne
    case sectionTwo
    case sectionThree
    case sectionFour
}

NSCollectionLayoutSection * _Nullable (^sectionProvider)(NSInteger, id<NSCollectionLayoutEnvironment> _Nonnull) = ^NSCollectionLayoutSection * _Nullable (NSInteger sectionIndex, id<NSCollectionLayoutEnvironment> _Nonnull layoutEnvironment) {
    // ================================================
    // this right here gets you correct section value
    let snapshot = self.datasource.snapshot()
    let section = snapshot.sectionIdentifiers[sectionIndex]
    // ================================================

    if (section == .sectionOne) {
        //configure and return a layout for the first section
    } else if (section == . sectionTwo) {
        //configure and return a layout for the second section
    } else if (section == . sectionThree) {
        //configure and return a layout for the third section
    } else if (section == . sectionFour) {
        //configure and return a layout for the fourth section
    }
    return nil;
};
almas
  • 7,090
  • 7
  • 33
  • 49