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

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)
        }
    })
}

效果说明

  1. 首次启动时两个监听器并行拉取数据,都完成首次加载后才会调用统一的 completion
  2. 后续任意监听器触发数据更新,都会跳过同步判断逻辑,各自处理完数据直接结束,完全独立运行
  3. 不会出现 DispatchGroup 调用次数不匹配的崩溃问题,也不会出现嵌套触发的问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 07:18:03