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

iOS 11中UITableView performBatchUpdates的completion闭包未调用

问题:UITableView.performBatchUpdates的completion闭包未被调用?

我有一个UITableView用于展示多个RSS源的聚合帖子流。每当应用从源拉取新帖子时,我会创建一个包含UITableView当前行所需的所有插入和删除操作的对象,并将这些对象放入FIFO队列中。每隔几秒,我会从队列取出首个元素,尝试通过UITableView:performBatchUpdates方法执行所有插入和删除操作。

一切正常,除非用户上下滚动UITableView。此时更新会停止,因为我设置了一个标志确保在上一批插入/删除完成后才启动下一批,但有时UITableView:performBatchUpdates的completion闭包从未被调用,导致标志无法清除。

以下是处理UITableView变更队列的代码:

@objc func updateFeedPostsTableView() {
    guard feedUpdateQueue.count > 0, !feedTableUpdateInProgress else { return }
    feedTableUpdateInProgress = true
    let feedUpdate = feedUpdateQueue.first!
    feedUpdateQueue.remove(at: 0)
    
    self.aggregatedRSSFeed = feedUpdate.feed
    self.feedPostsTableView.performBatchUpdates ({
        self.feedPostsTableView.deleteRows(at: feedUpdate.indexPathsOfDeletedPosts, with: .fade)
        self.feedPostsTableView.insertRows(at: feedUpdate.indexPathsOfNewPosts, with: .top)
    }, completion: { (success) in
        self.feedTableUpdateInProgress = false
    })
}

我的问题是:为何UITableView.performBatchUpdates会出现completion块不调用的情况?我哪里操作有误?

环境:iOS 11.2,Xcode 9.2。


解答

结合你的场景和iOS 11的已知问题,我来分析几个可能的原因和对应的解决办法:

1. 数据源更新与TableView操作的顺序不匹配

你当前的代码是先更新数据源self.aggregatedRSSFeed,再在performBatchUpdates的闭包里执行删除/插入操作。虽然苹果允许在调用方法前修改数据源,但如果你的indexPathsOfDeletedPosts/indexPathsOfNewPosts和更新后的数据源不匹配(比如删除的行已经不存在、插入的位置超出数据源行数),TableView会进入内部异常状态,直接跳过completion闭包的执行。

修复方案:把数据源的更新移到performBatchUpdates的闭包内部,确保数据源变更和TableView操作的原子性:

@objc func updateFeedPostsTableView() {
    guard feedUpdateQueue.count > 0, !feedTableUpdateInProgress else { return }
    feedTableUpdateInProgress = true
    let feedUpdate = feedUpdateQueue.first!
    feedUpdateQueue.remove(at: 0)
    
    self.feedPostsTableView.performBatchUpdates ({
        // 先更新数据源,确保和后续的TableView操作完全匹配
        self.aggregatedRSSFeed = feedUpdate.feed
        self.feedPostsTableView.deleteRows(at: feedUpdate.indexPathsOfDeletedPosts, with: .fade)
        self.feedPostsTableView.insertRows(at: feedUpdate.indexPathsOfNewPosts, with: .top)
    }, completion: { (success) in
        self.feedTableUpdateInProgress = false
    })
}

2. iOS 11滚动状态下的BatchUpdates Bug

iOS 11早期版本(包括你使用的11.2)存在一个已知问题:当TableView处于拖拽中或减速滚动状态时,调用performBatchUpdates可能会导致completion闭包完全不触发。这是因为系统在滚动时会暂停部分UI更新逻辑,但没有正确处理completion的回调。

修复方案:在执行更新前检查TableView的滚动状态,如果正在滚动/减速,延迟到滚动停止后再执行更新:

@objc func updateFeedPostsTableView() {
    guard feedUpdateQueue.count > 0, !feedTableUpdateInProgress else { return }
    
    // 检查滚动状态,避免在滚动时执行batchUpdates
    if self.feedPostsTableView.isDragging || self.feedPostsTableView.isDecelerating {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            self.updateFeedPostsTableView()
        }
        return
    }
    
    feedTableUpdateInProgress = true
    let feedUpdate = feedUpdateQueue.first!
    feedUpdateQueue.remove(at: 0)
    
    self.feedPostsTableView.performBatchUpdates ({
        self.aggregatedRSSFeed = feedUpdate.feed
        self.feedPostsTableView.deleteRows(at: feedUpdate.indexPathsOfDeletedPosts, with: .fade)
        self.feedPostsTableView.insertRows(at: feedUpdate.indexPathsOfNewPosts, with: .top)
    }, completion: { (success) in
        self.feedTableUpdateInProgress = false
        // 完成后立刻处理下一个更新,替代定时器触发
        self.updateFeedPostsTableView()
    })
}

这里我还把定时器触发改成了completion里递归调用,这样能更精准地控制更新的顺序,避免定时器和completion的时序冲突。

3. 空操作导致的Completion不触发

如果你的feedUpdate对象中,indexPathsOfDeletedPosts和indexPathsOfNewPosts都是空数组,那么performBatchUpdates实际上没有任何UI操作需要执行。在iOS 11的某些版本中,这种空操作会导致系统跳过completion闭包的调用,直接结束方法。

修复方案:在执行前先检查是否有实际的操作需要执行,如果没有,直接清除标志并处理下一个队列元素:

@objc func updateFeedPostsTableView() {
    guard feedUpdateQueue.count > 0, !feedTableUpdateInProgress else { return }
    
    let feedUpdate = feedUpdateQueue.first!
    // 检查是否有实际操作
    if feedUpdate.indexPathsOfDeletedPosts.isEmpty && feedUpdate.indexPathsOfNewPosts.isEmpty {
        feedUpdateQueue.remove(at: 0)
        // 直接处理下一个更新
        self.updateFeedPostsTableView()
        return
    }
    
    // 后续的滚动检查、batchUpdates逻辑...
}

4. 无效的IndexPath导致静默失败

如果你的indexPathsOfDeletedPosts包含了超出当前数据源范围的行,或者indexPathsOfNewPosts的位置不符合更新后的数据源行数,TableView会静默失败(不会崩溃,但也不会执行任何操作),同时跳过completion闭包。

检查建议:在执行deleteRows/insertRows前,添加日志或断言来验证IndexPath的有效性:

// 验证删除的IndexPath是否有效
for indexPath in feedUpdate.indexPathsOfDeletedPosts {
    assert(indexPath.row < self.aggregatedRSSFeed.count, "Invalid delete indexPath: \(indexPath)")
}
// 验证插入的IndexPath是否有效(更新数据源后)
self.aggregatedRSSFeed = feedUpdate.feed
for indexPath in feedUpdate.indexPathsOfNewPosts {
    assert(indexPath.row <= self.aggregatedRSSFeed.count, "Invalid insert indexPath: \(indexPath)")
}

内容的提问来源于stack exchange,提问作者Greg Anderson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:24:00