Swift: дождитесь загрузки 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 в блок завершения.

2 ответов


(вариации на этот вопрос возникают постоянно на SO. Я никогда не могу найти хороший, исчерпывающий ответ, поэтому ниже предпринята попытка дать такой ответ)

вы не можете этого сделать. Военнослужащих является асинхронным. Его функции принимают обработчик завершения и немедленно возвращаются. Нужно переписать функцию 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!
      }
    }

код выше использует URLSession класс для асинхронной загрузки данных с удаленного сервера. При создании dataTask, вы передаете обработчик завершения, который вызывается, когда задача данных завершена(или не выполнена.) Будьте осторожны: ваш обработчик завершения вызывается на фоне нитка.

это хорошо, потому что если вам нужно сделать трудоемкую обработку, такую как разбор больших структур JSON или XML, вы можете сделать это в обработчике завершения, не заставляя пользовательский интерфейс вашего приложения замерзать. Однако в результате вы не можете выполнять вызовы пользовательского интерфейса в обработчике завершения задачи данных без отправки этих вызовов пользовательского интерфейса в основной поток. Приведенный выше код вызывает весь обработчик завершения в основном потоке, используя вызов DispatchQueue.main.async() {}.

вернуться к операции код:

Я нахожу, что функцию с замыканием в качестве параметра трудно читать, поэтому я обычно определяю замыкание как тип.

переделка кода из ответа @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 к основному потоку.


Edit:

на основе ответов на это так вопрос, похоже, 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)
          }
        }
    })
}

что делает ответ Дункана более точным. Вы можете сделать функцию такой

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 в блоке обработчика завершения.