iOS15.2下UICollectionView批量删N项时末尾N项无法重载
iOS 15.2下UICollectionView批量更新异常问题
测试结论
- 经多系统版本测试,该更新异常仅出现在iOS 15.2系统,iOS 13.0、14.0系统下更新逻辑可正常运行,初步判断为iOS系统Bug。
问题表现
对UICollectionView执行performBatchUpdates批量更新时存在异常:若某分区内删除了N个元素,该分区末尾的N个元素会忽略reloadItems调用,无法正常触发重载刷新。
可稳定复现的示例代码
import UIKit final class CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { var data: [[String]] = [] var requestedIndexPaths: [IndexPath] = [] override func viewDidLoad() { super.viewDidLoad() collectionView.backgroundColor = .systemBackground collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell") applyInitialData( [ ["0, 0", "0, 1", "0, 2", "0, 3", "0, 4", "0, 5", "0, 6", "0, 7"], ] ) let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(reloadAll), for: .valueChanged) collectionView.refreshControl = refreshControl } @objc private func reloadAll() { collectionView.reloadData() collectionView.refreshControl?.endRefreshing() } func applyInitialData(_ data: [[String]]) { self.data = data loadViewIfNeeded() collectionView.reloadData() collectionView.layoutIfNeeded() requestedIndexPaths = [] } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { requestedIndexPaths.append(indexPath) let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for: indexPath) cell.contentView.subviews.forEach { $0.removeFromSuperview() } let label = UILabel() label.text = data[indexPath.section][indexPath.row] cell.contentView.addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: cell.topAnchor), label.leadingAnchor.constraint(equalTo: cell.leadingAnchor), ]) return cell } override func numberOfSections(in collectionView: UICollectionView) -> Int { data.count } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return data[section].count } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: 40) } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt: IndexPath) { updateUsingReload() // updateUsingInsertAndDelete() } /// 调用该方法时(0,6)位置单元格不会更新,下拉刷新后才会加载最新内容 private func updateUsingReload() { collectionView.performBatchUpdates({ data[0][0] = "0, 0 (updated)" data[0][1] = "0, 1 (updated)" data[0][4] = "0, 4 (updated)" data[0][5] = "0, 5 (updated)" data[0][6] = "0, 6 (updated)" data[0].remove(at: 3) data[0].remove(at: 2) collectionView.deleteItems(at: [ IndexPath(item: 2, section: 0), IndexPath(item: 3, section: 0), ]) collectionView.reloadItems(at: [ IndexPath(item: 0, section: 0), IndexPath(item: 1, section: 0), IndexPath(item: 4, section: 0), IndexPath(item: 5, section: 0), IndexPath(item: 6, section: 0), ]) }) } /// 调用该方法可重载所有单元格,但动画效果不符合预期,没有原生删除的自然动画 private func updateUsingInsertAndDelete() { collectionView.performBatchUpdates({ data[0][0] = "0, 0 (updated)" data[0][1] = "0, 1 (updated)" data[0][4] = "0, 4 (updated)" data[0][5] = "0, 5 (updated)" data[0][6] = "0, 6 (updated)" data[0].remove(at: 3) data[0].remove(at: 2) collectionView.deleteItems(at: [ IndexPath(item: 2, section: 0), IndexPath(item: 3, section: 0), IndexPath(item: 4, section: 0), IndexPath(item: 5, section: 0), IndexPath(item: 6, section: 0), ]) collectionView.insertItems(at: [ // 实际删除了2个元素,此处使用更新后的索引,对应更新前的4、5、6位置 IndexPath(item: 2, section: 0), IndexPath(item: 3, section: 0), IndexPath(item: 4, section: 0), ]) collectionView.reloadItems(at: [ IndexPath(item: 0, section: 0), IndexPath(item: 1, section: 0), ]) }) } }
复现逻辑
- 点击单元格触发
updateUsingReload更新逻辑时,(0,6)位置的单元格不会更新为最新内容 - 改用
updateUsingInsertAndDelete方法虽可重载所有单元格,但无法实现updateUsingReload自带的原生删除动画效果
诉求
寻找合规解决方案,在保留原生删除动画的前提下,让所有待更新单元格正常触发重载。
内容的提问来源于stack exchange,提问作者Joseph Duffy
相关产品推荐
相关产品推荐

