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

Swift类中Firestore异步Completion Handler未生效问题排查

问题分析与解决方案

你的问题核心在于异步操作的时机处理错误,以及单例初始化时对异步回调的误用,导致ViewController访问Pins数组时,Firestore的数据请求还没完成,所以拿到的是空数组。下面一步步拆解问题并给出修复方案:

1. 核心问题拆解

  • 单例初始化的异步误用:你在GetBottlesFromDB的init方法里直接调用了populatePinList,但Firestore的getDocuments是异步执行的——请求会在后台线程处理,而init是同步执行的,不会等待请求完成就继续往下走。而且你传入的completion闭包只是返回self.Pins,完全没起到等待回调的作用。
  • ViewController访问时机错误:当你在ViewController里直接访问GetBottlesFromDB.sharedInstance.Pins时,单例刚完成初始化,但Firestore的请求大概率还没结束,Pins自然是空的。

修复步骤

步骤1:修正populatePinList方法的逻辑

首先,你代码里的bottletitel和bottlesub没有初始化,会直接编译报错,先补上赋值;同时优化异步回调的处理,确保在所有数据处理完成后再返回结果,还要避免强制解包导致的崩溃:

func populatePinList(completion: @escaping (Result<[LayoutBottlesFromDB], Error>) -> ()) {
    Pins = []
    AppDelegate.ADglobalVar.db.collection("Bottles")
        .whereField("pickupuser", isEqualTo: NSNull())
        .getDocuments { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
                completion(.failure(err))
                return
            }
            
            guard let documents = querySnapshot?.documents else {
                completion(.success([]))
                return
            }
            
            print("start getting documents:")
            var fetchedPins = [LayoutBottlesFromDB]()
            for document in documents {
                let bottleID = document.documentID
                let bottlekind = document.data()["bottle"] as! Int
                // 处理可选值,避免强制解包崩溃
                let bottletitel = document.data()["title"] as? String ?? "Unknown Title"
                let bottlesub = document.data()["subtitle"] as? String ?? "Unknown Subtitle"
                let bottleurl = document.data()["pic"] as? String ?? ""
                
                let pin = LayoutBottlesFromDB(
                    document.data()["lat"] as! CLLocationDegrees,
                    document.data()["long"] as! CLLocationDegrees,
                    ID: bottleID,
                    title: bottletitel,
                    subtitle: bottlesub,
                    type: bottlekind,
                    url: bottleurl
                )
                fetchedPins.append(pin)
            }
            self.Pins = fetchedPins
            completion(.success(fetchedPins))
        }
}

这里用Result类型替代了简单的数组回调,能更清晰地处理成功和失败的场景。

步骤2:移除init里的异步调用,让ViewController主动触发

不要在单例的init里自动调用数据请求,而是让ViewController在需要数据的时候主动调用,并通过completion闭包等待结果:

// 修改GetBottlesFromDB的init方法
override init() {
    super.init()
    // 移除init里的populatePinList调用,让外部主动触发数据加载
}

然后在ViewController里这样使用:

GetBottlesFromDB.sharedInstance.populatePinList { result in
    switch result {
    case .success(let pins):
        // 数据加载完成,在这里处理pins
        for pin in pins {
            print("Add Pin : \(pin)")
            // 这里可以执行添加地图标注等业务逻辑
        }
        // 此时sharedInstance的Pins也已经同步更新
    case .failure(let error):
        print("Failed to fetch pins: \(error)")
    }
}

步骤3:可选优化——添加数据更新通知

如果你的应用需要在数据更新时自动通知多个页面,可以在populatePinList完成后发送通知:

// 在completion(.success(fetchedPins))之前添加
NotificationCenter.default.post(name: NSNotification.Name("PinsUpdated"), object: fetchedPins)

然后在ViewController里监听通知:

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(handlePinsUpdated(_:)), name: NSNotification.Name("PinsUpdated"), object: nil)
}

@objc func handlePinsUpdated(_ notification: Notification) {
    if let pins = notification.object as? [LayoutBottlesFromDB] {
        for pin in pins {
            print("Add Pin : \(pin)")
        }
    }
}

// 记得在页面销毁时移除监听
deinit {
    NotificationCenter.default.removeObserver(self)
}

总结

核心问题就是没有等待异步请求完成就访问数据,通过让ViewController主动触发请求并利用completion闭包(或通知)处理异步结果,就能解决数据为空的问题。另外还要注意处理可选值,避免强制解包导致的运行时崩溃。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:09:49