Swift Function Returning a Value from Asynchronous Firebase Call

Swift Function returning a value from asynchronous firebase call

The problem with

var recipients : [String] = [""]
DispatchQueue.main.async {
GetRecipientsFor(GroupChatID: gchatID) { result in
print(result)
recipients = result
}
}
print(recipients) // Completes before recipients = result

is that the last line is happening before the async call.

To explain futher print(recipients) happens before recipients = result. All logic using recipients needs to happen within that completion block. All you need to do is

func getRecipients(completion: @escaping ([String]) -> ()) {
var recipients : [String] = [""]
DispatchQueue.main.async {
GetRecipientsFor(GroupChatID: gchatID) { result in
print(result)
completion(result)
}
}
}

if you want to have further logic included you can call a function within the completion i.e. handleResults(result). I think it would be very beneficial to read more about closures/completion blocks/and async calls.

How can I return a function only when the async call in a loop is completed in SwiftUI and Firebase?

The enter line is at the wrong place, it must be inside the loop.

And the DispatchQueue.main closure is not needed

func getAllReviews(user: User) {
let reviewID = user.reviews
var reviews: [Review] = [Review]()
let group = DispatchGroup()

for i in reviewID {
print(i)
group.enter()
db.collection("reviews").document(i).getDocument {
(query, err) in
print("alive??")

if err != nil {
print("hello")
} else {
print("did it reach")

let userId = query!.get("user id") as? String ?? ""

let star = query!.get("star") as? Int ?? -1
let value = query!.get("value") as? Int ?? 0
let comments = query!.get("comments") as? String ?? ""
print(comments)
reviews.append(Review(id: i,userId: userId, star: star, value: value, comments: comments))
print(reviews)
print("does it come")
}
group.leave()
}
}
group.notify(queue: .main) {
completion(reviews)
}
}

Firebase function call return value in Swift

Phil Nash's explanation for the Swift side of things worked, but the issue lied in my Firebase function which I had to create a new function for based on the Twilio/Firebase Function API documentation:

exports.twilioTokenV2 = functions.https.onCall((data, context) => {
const AccessToken = twilio.jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;
const twilioAccountSid = functions.config().twilio_api.account_sid;
const twilioApiKey = functions.config().twilio_api.key;
const twilioApiSecret = functions.config().twilio_api.secret;
// Grab uid passed in for identity
const identity = data.uid;

// Grab question ID passed in for room name
const videoGrant = new VideoGrant({
room: data.qid,
});

// Create an access token which we will sign and return to the client,
// containing the grant we just created
const token = new AccessToken(twilioAccountSid, twilioApiKey, twilioApiSecret);
token.addGrant(videoGrant);
token.identity = identity;

console.log("Sending token: ", token);
return {
token: token.toJwt()
}

});

Return value from Firebase Database async method

You have to employ asynchronous completion handler yourself and verify if there is Internet connection:

func checkUsernameAlreadyTaken(username: String, completionHandler: (Bool) -> ()) {
databaseRef.child("usernames").child("\(username)").observe(.value, with: { (snapshot) in
print(username)
if snapshot.exists() {
completionHandler(false)
} else {
let connectedRef = FIRDatabase.database().reference(withPath: ".info/connected")
connectedRef.observe(.value, with: { snapshot in
if let connected = snapshot.value as? Bool, connected {
completionHandler(true)
} else {
completionHandler(false)
// Show a popup with the description
let alert = UIAlertController(title: NSLocalizedString("No connection", comment: "Title Internet connection error"), message: NSLocalizedString("No internet connection, please go online", comment: "Internet connection error saving/retriving data in Firebase Database"), preferredStyle: .alert)
let defaultOkAction = UIAlertAction(title: NSLocalizedString("No internet connection, please go online", comment: "Internet connection error saving/retriving data in Firebase Database"), style: .default, handler: nil)
alert.addAction(defaultOkAction)

self.present(alert, animated: true, completion: nil)
}
})
}
})
}

Then you call your method with:

checkIfUserExists(username: text, completionHandler: { (value) in
// ...
})

Wait for Firebase to load before returning from a function

(Variations on this question come up constantly on SO. I can never find a good, comprehensive answer, so below is an attempt to provide such an answer)

You can't do that. Firebase is asynchronous. Its functions take a completion handler and return immediately. You need to rewrite your loadFromFirebase function to take a completion handler.

I have a sample project on Github called Async_demo (link) that is a working (Swift 3) app illustrating this technique.

The key part of that is the function downloadFileAtURL, which takes a completion handler and does an async download:

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

The code above uses Apple's URLSession class to download data from a remote server asynchronously. When you create a dataTask, you pass in a completion handler that gets invoked when the data task has completed (or failed.) Beware, though: Your completion handler gets invoked on a background thread.

That's good, because if you need to do time-consuming processing like parsing large JSON or XML structures, you can do it in the completion handler without causing your app's UI to freeze. However, as a result you can't do UI calls in the data task completion handler without sending those UI calls to the main thread. The code above invokes the entire completion handler on the main thread, using a call to DispatchQueue.main.async() {}.

Back to the OP's code:

I find that a function with a closure as a parameter is hard to read, so I usually define the closure as a typealias.

Reworking the code from @Raghav7890's answer to use a typealias:

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

I haven't used Firebase in a long time (and then only modified somebody else's Firebase project), so I don't remember if it invokes it's completion handlers on the main thread or on a background thread. If it invokes completion handlers on a background thread then you may want to wrap the call to your completion handler in a GCD call to the main thread.


Edit:

Based on the answers to this SO question, it sounds like Firebase does it's networking calls on a background thread but invokes it's listeners on the main thread.

In that case you can ignore the code below for Firebase, but for those reading this thread for help with other sorts of async code, here's how you would rewrite the code to invoke the completion handler on the main thread:

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


Related Topics



Leave a reply



Submit