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
相关产品推荐
相关产品推荐

