iOS开发:如何让两个Firebase快照监听器启动时同步运行、后续独立执行
实现方案
核心问题分析
你之前的方案报错/不符合需求的根本原因是:Firebase 的 addSnapshotListener 是持久监听器,首次拉取数据会触发回调,后续数据变更也会重复触发回调。
- 嵌套回调方案:后续 own 数据变更触发回调时,会重复调用 fetchFriendsGames 逻辑
- DispatchGroup 方案:每次监听器触发都调用 leave,超过 enter 次数就会崩溃
解决思路
新增3个状态标记控制逻辑,仅在首次加载时做同步判断,后续触发更新完全独立运行。
具体代码实现
1. 新增类属性做状态控制
// 标记两个监听器各自的首次加载是否完成 private var isOwnFirstLoadDone = false private var isFriendFirstLoadDone = false // 标记是否已经完成首次同步加载 private var hasCompletedInitialFetch = false
2. 重写 fetchData 逻辑
func fetchData(completion: @escaping () -> Void) { // 仅当初始未加载、游戏列表为空时走首次同步逻辑 guard games.isEmpty, !hasCompletedInitialFetch else { return } // 检查两个监听器是否都完成首次加载 let checkInitialDone: () -> Void = { [weak self] in guard let self = self, self.isOwnFirstLoadDone, self.isFriendFirstLoadDone, !self.hasCompletedInitialFetch else { return } self.hasCompletedInitialFetch = true completion() } // 并行注册两个监听器 fetchOwnGames { [weak self] in guard let self = self else { return } // 仅首次加载走同步判断 if !self.isOwnFirstLoadDone { self.isOwnFirstLoadDone = true checkInitialDone() } // 后续监听器触发更新直接结束,独立运行 } fetchFriendsGames { [weak self] in guard let self = self else { return } if !self.isFriendFirstLoadDone { self.isFriendFirstLoadDone = true checkInitialDone() } } }
3. 优化 fetchFriendsGames 避免多次触发回调
你当前遍历多个朋友注册监听,每个朋友的监听器首次触发都会调用 completion,需要加内部标记保证仅首次触发一次外层回调:
func fetchFriendsGames(completion: @escaping () -> Void) { userService.fetchFriends(friendCompletion: { [self] friends in let friendsUidArray = friends.compactMap { $0.id } guard !friendsUidArray.isEmpty else { completion() return } // 内部标记,避免多个朋友的监听器首次触发重复调用completion var isFriendFirstCallbackTriggered = false for uid in friendsUidArray { // 注意:你当前friendListener是单个变量,会覆盖前面的监听导致泄漏,建议改成数组存储所有监听器 let listener = Constants.FirebaseCollection.gamesCollection .order(by: "startTime", descending: true) .whereField("ownerUid", isEqualTo: uid) .limit(to: 5) .addSnapshotListener({ querySnapshot, error in guard let querySnapshot = querySnapshot, error == nil else { print("DEBUG: error", error?.localizedDescription as Any) return } // 原有数据处理逻辑不变 querySnapshot.documentChanges.forEach { change in switch change.type { case .added: guard let data = try? change.document.data(as: Game.self) else { return } self.games.append(data) self.friendGames.append(data) self.games = games.sorted(by: { $0.startTime.compare($1.startTime) == .orderedDescending}) case .modified: guard let data = try? change.document.data(as: Game.self) else { return } if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) { self.games[index] = data } case .removed: guard let data = try? change.document.data(as: Game.self) else { return } self.games = self.games.filter { $0 != data } self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending}) } } print("DEBUG3: friendgames count", friendGames.count) // 仅首次回调调用一次completion if !isFriendFirstCallbackTriggered { isFriendFirstCallbackTriggered = true completion() } }) // 建议把所有朋友的监听器存入数组,页面销毁时统一移除避免泄漏 // self.friendListeners.append(listener) } }) }
效果说明
- 首次启动时两个监听器并行拉取数据,都完成首次加载后才会调用统一的 completion
- 后续任意监听器触发数据更新,都会跳过同步判断逻辑,各自处理完数据直接结束,完全独立运行
- 不会出现 DispatchGroup 调用次数不匹配的崩溃问题,也不会出现嵌套触发的问题
内容的提问来源于stack exchange,提问作者tonileipo
相关产品推荐
相关产品推荐

