如何解决BLE回调场景下Table View滚动持有时的Cell闪烁问题?
咱们先来理清楚问题的根源:你用定时器刷新的时候,系统默认会把定时器加入RunLoop的.commonModes,当TableView处于拖动或者减速滚动状态时,RunLoop会切换到.trackingRunLoopMode,这时候commonMode的定时器就会暂停,所以不会触发刷新,自然没闪烁。但BLE的回调不一样,它一般是在后台处理完数据后直接切到主线程执行刷新,不管RunLoop当前是什么模式,所以哪怕你按住滚动TableView,回调还是会持续触发reloadRows,频繁的刷新操作就导致了Cell闪烁。
接下来给你几个可行的解决方案,结合你的代码来改:
方案一:判断滚动状态,滚动时暂存更新,停止后再刷新
这个思路是:先保证数据源始终是最新的,然后判断TableView是否在滚动,如果在滚动就标记需要刷新,等滚动停止后再统一更新UI。
步骤1:添加一个标记变量
在你的ViewController里加一个布尔值,用来记录是否需要在滚动停止后刷新:
private var needsReloadAfterScroll = false
步骤2:修改BLE回调的刷新方法
把原来的刷新逻辑拆成更新数据源和条件刷新UI两部分:
@objc func reloadTableViewCellsOnBleScan(_ name: String) { // 第一步:先更新数据源,确保数据是最新的 var targetIndex: IndexPath? if let index = myArray.firstIndex(where: { $0 == name }) { myArray[index] = name targetIndex = IndexPath(row: index, section: 0) } else { myArray.append(name) needsReloadAfterScroll = true } // 第二步:判断TableView是否处于静止状态,只有静止时才立即刷新 if !tableViewRx.isDragging && !tableViewRx.isDecelerating { if let indexPath = targetIndex { tableViewRx.reloadRows(at: [indexPath], with: .none) } else { reloadTableView() } needsReloadAfterScroll = false // 刷新后重置标记 } else { // 如果正在滚动,标记需要刷新 if targetIndex != nil { needsReloadAfterScroll = true } } }
步骤3:实现TableView滚动停止的代理方法
让你的ViewController遵守UITableViewDelegate(如果还没遵守的话),然后添加以下代理方法,处理滚动停止后的刷新:
extension YourViewController: UITableViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { // 如果拖动后直接停止,没有减速,就立即刷新 if !decelerate && needsReloadAfterScroll { updateTableViewAfterScroll() } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { // 滚动减速停止后刷新 if needsReloadAfterScroll { updateTableViewAfterScroll() } } // 封装一个统一的刷新方法 private func updateTableViewAfterScroll() { // 这里可以选择整体reload,或者更精准地刷新变化的行 // 如果要精准刷新,你可以额外记录变化的indexPath数组,这里为了简单用reloadData tableViewRx.reloadData() needsReloadAfterScroll = false } }
方案二:给BLE回调的刷新加防抖(可选)
因为BLE允许重复扫描的话,可能会在短时间内多次回调同一个设备的信息,频繁刷新同一个Cell也会导致闪烁。你可以加一个防抖机制,比如用GCD的定时器,确保100ms内只刷新一次:
private var debounceTimer: Timer? @objc func reloadTableViewCellsOnBleScan(_ name: String) { // 先更新数据源 var targetIndex: IndexPath? if let index = myArray.firstIndex(where: { $0 == name }) { myArray[index] = name targetIndex = IndexPath(row: index, section: 0) } else { myArray.append(name) needsReloadAfterScroll = true } // 取消之前的定时器 debounceTimer?.invalidate() // 新建定时器,100ms后执行刷新 debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { [weak self] _ in guard let self = self else { return } if !self.tableViewRx.isDragging && !self.tableViewRx.isDecelerating { if let indexPath = targetIndex { self.tableViewRx.reloadRows(at: [indexPath], with: .none) } else { self.reloadTableView() } self.needsReloadAfterScroll = false } else { self.needsReloadAfterScroll = true } } }
这个方案可以和方案一结合使用,进一步减少不必要的刷新操作,避免闪烁。
为什么定时器场景没问题?
再补充一下你提到的定时器场景的原理:Timer.scheduledTimer默认会把定时器加入RunLoop的.commonModes,当TableView滚动时,RunLoop会切换到.trackingRunLoopMode,此时commonMode的定时器会暂停,直到滚动停止后才继续触发,所以不会有闪烁的问题。而BLE回调是直接在主线程执行,不受RunLoop模式的影响,所以滚动时还是会触发刷新。
内容的提问来源于stack exchange,提问作者yuvaraju

