Renzo Tissoni
Renzo Tissoni

Reputation: 759

UICollectionView glitches when removing sections using Compositional Layouts and diffable data sources

I'm experiencing a problem when rendering removable sections in my collection views. The setup is as follows:

The collection view has two sections. The first one contains only one cell, which when tapped triggers a snapshot update that removes the sections from the collection view. The second section contains several cells scrolling orthogonally.

The problem occurs when scrolling the second section, and then tapping the only cell on the first one.

The behavior I expect is that the first section would be removed, and the section would slide into the first's position in a smooth animation.

What I'm seeing is the orthogonally scrolling section flickering and jumping until the animation is complete.

Here are some examples of what I'd expect, and what happens.

Expected (without scrolling) Unexpected (actual result)
Expected Unexpected

Here's a code snippet ready to drop on a Playground to test (Xcode 15.3, iOS 17). Please note that it's just an example I tried to distill from the real code, which is more complex.

import UIKit
import PlaygroundSupport

var onTap = {
    removeSection()
}

// Rainbow colored cells
final class Cell: UICollectionViewCell {
    override func prepareForReuse() {
        contentView.backgroundColor = .clear
    }

    func configure(with color: UIColor) {
        contentView.backgroundColor = color
    }
}

// The cell that when tapped, triggers the section removal (note that selection isn't being handled through didSelectItem.
final class RemovableCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .systemCyan
        contentView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc
    func tap() {
        onTap()
    }
}

// Registrations for the cells
let cellRegistration = UICollectionView.CellRegistration<Cell, Int> { cell, indexPath, _ in
    let colors: [UIColor] = [.red, .orange, .yellow, .green, .blue]
    cell.configure(with: colors[indexPath.item % colors.count])
}
let removableCellRegistration = UICollectionView.CellRegistration<RemovableCell, Int> { cell, indexPath, _ in }

let removableSection: NSCollectionLayoutSection = {
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .estimated(120))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)

    let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
    let section = NSCollectionLayoutSection(group: group)

    return section
}()

// Sections definitions
let orthogonallyScrollingSection: NSCollectionLayoutSection = {
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(1))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)

    let groupSize = NSCollectionLayoutSize(
        widthDimension: .estimated(100),
        heightDimension: .fractionalHeight(0.2))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .continuous

    return section
}()

// Collection view construction, please don't mind the mess.
var removeSectionAvailable = true
let layout = UICollectionViewCompositionalLayout { section, _ in
    if section == 0, removeSectionAvailable {
        removableSection
    } else {
        orthogonallyScrollingSection
    }
}
let frame = CGRect(x: 0, y: 0, width: 300, height: 500)
let view = UICollectionView(frame: frame, collectionViewLayout: layout)
let dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: view) { collectionView, indexPath, itemIdentifier in
    if indexPath.section == 0, removeSectionAvailable {
        return collectionView.dequeueConfiguredReusableCell(
            using: removableCellRegistration,
            for: indexPath,
            item: itemIdentifier)
    } else {
        return collectionView.dequeueConfiguredReusableCell(
            using: cellRegistration,
            for: indexPath,
            item: itemIdentifier
        )
    }
}

var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
snapshot.appendSections([0, 1])
snapshot.appendItems([0], toSection: 0)
snapshot.appendItems([1, 2, 3, 4, 5, 6], toSection: 1)
dataSource.apply(snapshot)

func removeSection() {
    removeSectionAvailable = false
    snapshot.deleteSections([0])
    dataSource.apply(snapshot)
}

// Playground setup
PlaygroundPage.current.liveView = view
PlaygroundPage.current.needsIndefiniteExecution = true

All tips are appreciated!

Upvotes: 2

Views: 121

Answers (0)

Related Questions