You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Compositional Layout和Diffable DataSource移除UICollectionView分区时异常

可移除分区的UICollectionView正交滚动分区动画异常问题

场景说明

UICollectionView包含两个分区:

  • 第一个分区仅含一个单元格,点击该单元格会触发快照更新以移除该分区
  • 第二个分区包含多个可正交滚动的单元格

问题现象

滚动第二个分区后,点击第一个分区的单元格时:

  • 预期行为:第一个分区平滑移除,第二个分区滑动至原第一个分区的位置
  • 实际现象:正交滚动分区出现闪烁、跳动,直到动画结束

效果对比

预期效果(未滚动时)异常效果(实际结果)
预期效果异常效果

测试代码(Xcode 15.3,iOS 17,可直接在Playground运行)

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

恳请各位提供解决思路!


内容的提问来源于stack exchange,提问作者Renzo Tissoni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:34:53