Stefan Arn Stefan Arn - 5 months ago 57
iOS Question

How can UICollectionView supplementary views be inserted or deleted correctly

Short question:

Is there a way to add and remove supplementary views like cells and section in a

performBatchUpdates:
block similar to the
insertItemsAtIndexPaths:
deleteItemsAtIndexPaths:
or even
reloadItemsAtIndexPaths:
methods?

Long explanation:

I use a
UICollectionView
with sections. The cells are laid out like a grid and each line can have up to 6 cells. I add additional supplementary views if the total amount of cells do not fill up all lines of a section. The additional supplementary views just fill up the empty space. So the result should be that each section is completely filled with all available cells and the (possible) remaining empty spots are filled with supplementary views. So each line has 6 visual elements, eather a cell or a supplementary view.

To achieve that I implemented a custom
UICollectionViewFlowLayout
based layout with the following
prepareLayout:
implementation:

- (void)prepareLayout {
[super prepareLayout];

self.supplementaryViewAttributeList = [NSMutableArray array];
if(self.collectionView != nil) {
// calculate layout values
CGFloat contentWidth = self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right;
CGFloat cellSizeWithSpacing = self.itemSize.width + self.minimumInteritemSpacing;
NSInteger numberOfItemsPerLine = floor(contentWidth / cellSizeWithSpacing);
CGFloat realInterItemSpacing = (contentWidth - (numberOfItemsPerLine * self.itemSize.width)) / (numberOfItemsPerLine - 1);

// add supplementary attributes
for (NSInteger numberOfSection = 0; numberOfSection < self.collectionView.numberOfSections; numberOfSection++) {
NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:numberOfSection];
NSInteger numberOfSupplementaryViews = numberOfItemsPerLine - (numberOfItems % numberOfItemsPerLine);

if (numberOfSupplementaryViews > 0 && numberOfSupplementaryViews < 6) {
NSIndexPath *indexPathOfLastCellOfSection = [NSIndexPath indexPathForItem:(numberOfItems - 1) inSection:numberOfSection];
UICollectionViewLayoutAttributes *layoutAttributesOfLastCellOfSection = [self layoutAttributesForItemAtIndexPath:indexPathOfLastCellOfSection];

for (NSInteger numberOfSupplementor = 0; numberOfSupplementor < numberOfSupplementaryViews; numberOfSupplementor++) {
NSIndexPath *indexPathOfSupplementor = [NSIndexPath indexPathForItem:(numberOfItems + numberOfSupplementor) inSection:numberOfSection];
UICollectionViewLayoutAttributes *supplementaryLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:ARNCollectionElementKindFocusGuide withIndexPath:indexPathOfSupplementor];
supplementaryLayoutAttributes.frame = CGRectMake(layoutAttributesOfLastCellOfSection.frame.origin.x + ((numberOfSupplementor + 1) * (self.itemSize.width + realInterItemSpacing)), layoutAttributesOfLastCellOfSection.frame.origin.y, self.itemSize.width, self.itemSize.height);
supplementaryLayoutAttributes.zIndex = -1;

[self.supplementaryViewAttributeList addObject:supplementaryLayoutAttributes];
}
}
}
}
}


This works great if all data is available from the beginning and if there is no change to the data. But it fails miserably if data gets added and removed during the lifetime. Inserts and deletes of cells in a
performBatchUpdates:
mess up the supplementary views.

What should happen:

For every cell that is inserted with
insertItemsAtIndexPaths:
the supplementary view at the same
indexPath
should be removed. Furthermore for every cell that is deleted with
deleteItemsAtIndexPaths:
a new supplementary view should be added at the same
indexPath
.

The Problem:

The
prepareLayout:
of my layout is called and calculates the correct
layoutAttributes
. But
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
is called for weird
indexPaths
. Specifically the
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
is called for old
indexPaths
which got removed from the
supplementaryViewAttributeList
array!

Example:

In the case where a new cell gets inserted, the
prepareLayout:
correctly removes the
layoutAttributes
, but the
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
still gets called for the not longer available and deleted indexPath! And only for that. There is no additional call for any other
indexPath
s for all the other supplementary views.

Why is
layoutAttributesForSupplementaryViewOfKind:atIndexPath:
called for
indexPaths
that are not available any more? How would I delete, insert or reload the supplementary views correctly? What do I miss?

Source:

The full source can be found here:

Custom Layout: https://github.com/sarn/ARNClassicFilms/blob/master/classicFilms/classicFilms/layout/ARNCollectionViewFocusGuideFlowLayout.m

CollectionView Controller:
https://github.com/sarn/ARNClassicFilms/blob/master/classicFilms/classicFilms/view-controllers/ARNMovieOverviewController.m
(
performBatchUpdates:
is in the last method of the class)

Answer

One solution is to add a [[collectionView collectionViewLayout] invalidateLayout] to the performBatchUpdates: block. This forces the layout to recalculate and drop the not valid indexPaths.