Swift: Attendi che Firebase si carichi prima di restituire una funzione

Ho una semplice data di caricamento della funzione da Firebase.

func loadFromFireBase() -> Array? { var songArray:Array = [] ref.observe(.value, with: { snapshot in //Load songArray }) if songArray.isEmpty { return nil } return songArray } 

Attualmente questa funzione ritorna sempre anche se ci sono dati da caricare. Lo fa perché non raggiunge mai il blocco di completamento in cui carica l’array prima che la funzione ritorni. Sto cercando un modo per far sì che la funzione ritorni solo dopo che è stato chiamato il blocco di completamento, ma non riesco a inserire il risultato nel blocco di completamento.

(Le variazioni su questa domanda si verificano costantemente su SO. Non riesco mai a trovare una risposta valida e completa, quindi di seguito è un tentativo di fornire una risposta del genere)

Non puoi farlo. Firebase è asincrono. Le sue funzioni richiedono un gestore di completamento e ritornano immediatamente. È necessario riscrivere la funzione loadFromFirebase per assumere un gestore di completamento.

Ho un progetto di esempio su Github chiamato Async_demo (link) che è un’app funzionante (Swift 3) che illustra questa tecnica.

La parte fondamentale di ciò è la funzione downloadFileAtURL , che accetta un gestore di completamento e esegue un download asincrono:

 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! } } 

Il codice sopra riportato utilizza la class URLSession di Apple per scaricare dati da un server remoto in modo asincrono. Quando si crea un dataTask , si passa a un gestore di completamento che viene richiamato quando l’attività di dati è stata completata (o non è riuscita). Attenzione, tuttavia: il gestore di completamento viene richiamato su un thread in background.

Va bene, perché se devi eseguire un’elaborazione che richiede tempo come l’analisi di grandi strutture JSON o XML, puoi farlo nel gestore di completamento senza bloccare l’interfaccia utente della tua app. Tuttavia, di conseguenza non è ansible effettuare chiamate UI nel gestore di completamento attività dati senza inviare quelle chiamate UI al thread principale. Il codice sopra richiama l’intero gestore di completamento sul thread principale, utilizzando una chiamata a DispatchQueue.main.async() {} .

Torna al codice OP:

Trovo che una funzione con una chiusura come parametro sia difficile da leggere, quindi di solito definisco la chiusura come una tipografia.

Rielaborazione del codice dalla risposta @ di Raghav7890 per utilizzare una tipografia:

 typealias SongArrayClosure = (Array?) -> Void func loadFromFireBase(completionHandler: @escaping SongArrayClosure) { ref.observe(.value, with: { snapshot in var songArray:Array = [] //Put code here to load songArray from the FireBase returned data if songArray.isEmpty { completionHandler(nil) }else { completionHandler(songArray) } }) } 

Non ho usato Firebase da molto tempo (e poi ho modificato solo il progetto Firebase di qualcun altro), quindi non ricordo se invoca i suoi gestori di completamento sul thread principale o su un thread in background. Se invoca i gestori di completamento su un thread in background, è ansible che si desideri racchiudere la chiamata al gestore di completamento in una chiamata GCD al thread principale.


Modificare:

Basandosi sulle risposte a questa domanda SO , sembra che Firebase faccia le chiamate di rete su un thread in background ma invochi i suoi ascoltatori sul thread principale.

In questo caso puoi ignorare il codice sottostante per Firebase, ma per coloro che leggono questo thread per aiuto con altri tipi di codice asincrono, ecco come riscriverebbe il codice per richiamare il gestore di completamento sul thread principale:

 typealias SongArrayClosure = (Array?) -> Void func loadFromFireBase(completionHandler:@escaping SongArrayClosure) { ref.observe(.value, with: { snapshot in var songArray:Array = [] //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) } } }) } 

Rendere la risposta di Duncan più precisa. Puoi fare la funzione in questo modo

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

Puoi restituire il songArray in un blocco del gestore di completamento.