在从函数返回之前等待 Firebase 加载

Posted

技术标签:

【中文标题】在从函数返回之前等待 Firebase 加载【英文标题】:Wait for Firebase to load before returning from a function 【发布时间】:2017-05-06 21:04:12 【问题描述】:

我有一个从 Firebase 加载数据的简单函数。

func loadFromFireBase() -> Array<Song>? 
    var songArray:Array<Song> = []

    ref.observe(.value, with:  snapshot in
        //Load songArray
    )

    if songArray.isEmpty 
        return nil
    
    return songArray

目前这个函数总是返回nil,即使有数据要加载。它这样做是因为它永远不会到达执行完成块,它在函数返回之前加载数组。我正在寻找一种方法使函数仅在调用完成块后才返回,但我不能将 return 放入完成块中。

【问题讨论】:

【参考方案1】:

(关于这个问题的变体在 SO 上不断出现。我永远找不到一个好的、全面的答案,所以下面试图提供这样一个答案)

你不能那样做。 Firebase 是异步的。它的函数接受一个完成处理程序并立即返回。您需要重写 loadFromFirebase 函数以获取完成处理程序。

我在 Github 上有一个名为 Async_demo(链接)的示例项目,它是一个可工作的 (Swift 3) 应用程序,说明了这种技术。

其中的关键部分是函数downloadFileAtURL,它接受一个完成处理程序并进行异步下载:

typealias DataClosure = (Data?, Error?) -> Void

/**
 This class is a trivial example of a class that handles async processing. It offers a single function, `downloadFileAtURL()`
 */
class DownloadManager: NSObject 

  static var downloadManager = DownloadManager()

  private lazy var session: URLSession = 
    return URLSession.shared
  ()

    /**
     This function demonstrates handling an async task.
     - Parameter url The url to download
     - Parameter completion: A completion handler to execute once the download is finished
     */

      func downloadFileAtURL(_ url: URL, completion: @escaping DataClosure) 

        //We create a URLRequest that does not allow caching so you can see the download take place
        let request = URLRequest(url: url,
                                 cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                                 timeoutInterval: 30.0)
        let dataTask = URLSession.shared.dataTask(with: request) 
          //------------------------------------------
          //This is the completion handler, which runs LATER,
          //after downloadFileAtURL has returned.
          data, response, error in

          //Perform the completion handler on the main thread
          DispatchQueue.main.async() 
            //Call the copmletion handler that was passed to us
            completion(data, error)
          
          //------------------------------------------
        
        dataTask.resume()

        //When we get here the data task will NOT have completed yet!
      
    

上面的代码使用 Apple 的 URLSession 类从远程服务器异步下载数据。当您创建 dataTask 时,您会传入一个完成处理程序,该处理程序会在数据任务完成(或失败)时被调用。不过请注意:您的完成处理程序会在后台线程上被调用。

这很好,因为如果您需要进行耗时的处理,例如解析大型 JSON 或 XML 结构,您可以在完成处理程序中完成,而不会导致应用程序的 UI 冻结。但是,如果不将这些 UI 调用发送到主线程,您将无法在数据任务完成处理程序中执行 UI 调用。上面的代码调用主线程上的整个完成处理程序,使用对DispatchQueue.main.async() 的调用。

回到 OP 的代码:

我发现带有闭包作为参数的函数很难阅读,所以我通常将闭包定义为类型别名。

修改@Raghav7890 的答案中的代码以使用类型别名:

typealias SongArrayClosure = (Array<Song>?) -> Void

func loadFromFireBase(completionHandler: @escaping SongArrayClosure) 
    ref.observe(.value, with:  snapshot in
        var songArray:Array<Song> = []
        //Put code here to load songArray from the FireBase returned data

        if songArray.isEmpty 
            completionHandler(nil)
        else 
            completionHandler(songArray)
        
    )

我很久没有使用 Firebase(然后只修改了别人的 Firebase 项目),所以我不记得它是在主线程还是在后台线程调用它的完成处理程序。如果它在后台线程上调用完成处理程序,那么您可能希望将对完成处理程序的调用包装在对主线程的 GCD 调用中。


编辑:

根据对this SO question 的回答,听起来 Firebase 是在后台线程上进行网络调用,但在主线程上调用它的侦听器。

在这种情况下,您可以忽略下面的 Firebase 代码,但对于那些阅读此线程以寻求其他类型异步代码帮助的人,您可以通过以下方式重写代码以在主线程上调用完成处理程序:

typealias SongArrayClosure = (Array<Song>?) -> Void

func loadFromFireBase(completionHandler:@escaping SongArrayClosure) 
    ref.observe(.value, with:  snapshot in
        var songArray:Array<Song> = []
        //Put code here to load songArray from the FireBase returned data

        //Pass songArray to the completion handler on the main thread.
        DispatchQueue.main.async() 
          if songArray.isEmpty 
            completionHandler(nil)
          else 
            completionHandler(songArray)
          
        
    )

【讨论】:

能否解释一下为什么在这种情况下需要调用 Dispatch.main.async data, response, error in //Perform the completion handler on the main thread DispatchQueue.main.async() ?在in 关键字之后运行的代码已经在主线程上运行。为什么一定要打DispatchQueue.main.async() 简短的回答是您不必为此 Firebase 调用使用 DispatchQueue.main.async(),因为 Firebase 在主线程上调用它的完成处理程序。 (请参阅我编辑中的评论。)不过,我以这种方式保留了代码,因为对于其他类型的异步调用(如 Apple 的 URLSession 完成处理程序),您 确实 必须将 UIKit 调用包装在对DispatchQueue.main.async().【参考方案2】:

让邓肯的回答更准确。你可以像这样制作函数

func loadFromFireBase(completionHandler:@escaping (_ songArray: [Song]?)->()) 
    ref.observe(.value)  snapshot in
        var songArray: [Song] = []
        //Load songArray
        if songArray.isEmpty 
            completionHandler(nil)
        else 
            completionHandler(songArray)
        
    

您可以在完成处理程序块中返回 songArray。

【讨论】:

谢谢拉加夫。当我第一次写这个答案时,我不在我的 Mac 上,我正准备提供一个更完整的答案,但你打败了我。 在我看来,您在函数定义中的 completionHandler 周围多了一层括号。这使得它更难阅读,IMO。 @DuncanC 因为 Firebase 在主线程上调用它的监听器,我认为没有理由在主线程上调用 completionHandler。

以上是关于在从函数返回之前等待 Firebase 加载的主要内容,如果未能解决你的问题,请参考以下文章

将 Firebase 文档计数作为表行计数返回

如何在从 Meteor.method 返回之前等待子流程结果

在 forEach 之后使用 Promise.all() 渲染 NodeJS 页面之前等待 Firebase 数据加载

函数等待返回直到 $.getJSON 完成

Flutter Firebase 上传多张图片

等待 firebase 加载,直到显示 View