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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 22:01:06