web-dev-qa-db-ja.com

関数から戻る前にFirebaseが読み込まれるのを待ちます

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を入れることができません。

12
Lewis Black

(この質問のバリエーションは常に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呼び出しをメインスレッドに送信せずに実行することはできません。上記のコードは、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)
          }
        }
    })
}
_
13
Duncan C

ダンカンをより正確に答えさせる。あなたはこのような機能を作ることができます

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を返すことができます。

3
Raghav7890