如何修复Firebase数据拉取中Completion Block多次触发问题?
嘿,这个问题我太熟了——之前用Firebase做附近内容列表的时候也踩过一模一样的坑!每拉一条数据就刷新一次,不仅用户体验卡顿,还容易导致UI错乱。用DispatchGroup刚好能完美解决,核心思路就是把所有异步请求「打包」,等全部完成后再统一返回结果。
下面是具体的实现方案:
第一步:修改PostService.showEvent函数,加入DispatchGroup控制
我们需要把原来逐个返回结果的逻辑,改成先收集所有Event数据,等全部请求完成后再一次性触发completion:
class PostService { static func showEvent(range: Double, location: CLLocation, completion: @escaping ([Event]) -> Void) { // 1. 创建DispatchGroup追踪所有异步请求 let requestGroup = DispatchGroup() var collectedEvents: [Event] = [] let geoFire = GeoFire(firebaseRef: Database.database().reference().child("geoEvents")) geoFire.query(at: location, withRadius: range).observe(.keyEntered) { key, _ in // 2. 每发起一个Event请求前,标记进入Group requestGroup.enter() EventService.show(eventId: key) { fetchedEvent in defer { // 4. 不管请求成功/失败,都标记离开Group(避免死锁) requestGroup.leave() } guard let event = fetchedEvent else { return } collectedEvents.append(event) } } // 3. 监听Group完成通知,所有请求结束后统一返回结果 requestGroup.notify(queue: .main) { completion(collectedEvents) } } }
第二步:修改grabUserLoc中的调用逻辑
现在showEvent只会在所有Event都拉取完成后触发一次completion,你只需要在回调里刷新一次collectionView即可:
func grabUserLoc() { guard let userLocation = userLocation else { return } PostService.showEvent(range: 10, location: userLocation) { allEvents in // 这里只会执行一次! self.events = allEvents self.collectionView.reloadData() } }
补充细节说明
- 用
defer { requestGroup.leave() }是个关键习惯:确保不管请求成功、失败(比如某个Event数据不存在),都能正确调用leave(),避免Group一直等待导致死锁。 requestGroup.notify(queue: .main)必须指定主队列:因为刷新UI的操作必须在主线程执行,否则会引发崩溃或UI异常。- 如果需要处理部分请求失败的场景(比如跳过拉取失败的Event),可以在
EventService.show的回调里根据业务逻辑调整,比如失败时不添加到数组,但依然要调用leave()。
这样修改后,你的collectionView就只会刷新一次,用户体验会顺畅很多!
内容的提问来源于stack exchange,提问作者Ron Baker
相关产品推荐
相关产品推荐

