Finish Asynchronous Task in Firebase With Swift

Firebase & Swift: Asynchronous calls, Completion Handlers

Tomte's answer solved one of my problems (thank you!). The calculations will be carried out after userAranking and userBranking are received with this code:

while let nextMeme = enumerator.nextObject() as? DataSnapshot {
let group = DispatchGroup()

group.enter()
self.getRankingA(userA: userA, memeID: nextMeme.key) {
self.rankingAreceived = true
group.leave()
}

group.enter()
self.getRankingB(userB: userB, memeID: nextMeme.key) {
self.rankingBreceived = true
group.leave()
}

// is called when the last task left the group
group.notify(queue: .main) {
self.calculation()
}

}

Still, the completion call to updateScores would happen at the end of the loop but before all of the userArankings and userBrankings are received and before the rankings undergo calculations. I solved this problem by adding another dispatch group:

let downloadGroup = DispatchGroup()

while let nextMeme = enumerator.nextObject() as? DataSnapshot {
let calculationGroup = DispatchGroup()

downloadGroup.enter()
calculationGroup.enter()
self.getRankingA(userA: userA, memeID: nextMeme.key) {
downloadGroup.leave()
calculationGroup.leave()
}

downloadGroup.enter()
calculationGroup.enter()
self.getRankingB(userB: userB, memeID: nextMeme.key) {
downloadGroup.leave()
calculationGroup.leave()
}

// is called when the last task left the group
downloadGroup.enter()
calculationGroup.notify(queue: .main) {
self.calculation() {
downloadGroup.leave()
}
}

}

downloadGroup.notify(queue: .main) {
completion(self, userA, userB)
}

I had to add a completion handler to the calculation method as well to ensure that the updateScores method would be called once userAranking and userBranking undergo calculations, not just once they are received from the database.

Yay for dispatch groups!

Finish all asynchronous requests before loading data?

For users seeking answer to question in title then use of dispatch_group and GCD outlined here: i.e embedding one group inside the notification method of another dispatch_group is valid. Another way to go at a higher level would be NSOperations and dependencies which would also give further control such as canceling operations.

Outline:

func doStuffonObjectsProcessAndComplete(arrayOfObjectsToProcess: Array) -> Void){

let firstGroup = dispatch_group_create()

for object in arrayOfObjectsToProcess {

dispatch_group_enter(firstGroup)

doStuffToObject(object, completion:{ (success) in
if(success){
// doing stuff success
}
else {
// doing stuff fail
}
// regardless, we leave the group letting GCD know we finished this bit of work
dispatch_group_leave(firstGroup)
})
}

// called once all code blocks entered into group have left
dispatch_group_notify(firstGroup, dispatch_get_main_queue()) {

let processGroup = dispatch_group_create()

for object in arrayOfObjectsToProcess {

dispatch_group_enter(processGroup)

processObject(object, completion:{ (success) in
if(success){
// processing stuff success
}
else {
// processing stuff fail
}
// regardless, we leave the group letting GCD know we finished this bit of work
dispatch_group_leave(processGroup)
})
}

dispatch_group_notify(processGroup, dispatch_get_main_queue()) {
print("All Done and Processed, so load data now")
}
}
}

The remainder of this answer is specific to this codebase.

There seem to be a few problems here:
The getAttendees function takes an event child and returns an objectID and Name which are both Strings? Shouldn't this method return an array of attendees? If not, then what is the objectID that is returned?

Once an array of attendees is returned, then you can process them in a group to get the pictures.

The getAttendeesPictures eventually returns UIImages from Facebook. It's probably best to cache these out to the disk and pass path ref - keeping all these fetched images around is bad for memory, and depending on size and number, may quickly lead to problems.

Some examples:

func getAttendees(child: String, completion: (result: Bool, attendees: Array?) -> Void){

let newArrayOfAttendees = []()

// Get event attendees of particular event

// process attendees and package into an Array (or Dictionary)

// completion
completion(true, attendees: newArrayOfAttendees)
}

func getAttendeesPictures(attendees: Array, completion: (result: Bool, attendees: Array)-> Void){

println("Attendees Count: \(attendees.count)")

let picturesGroup = dispatch_group_create()

for attendee in attendees{

// for each attendee enter group
dispatch_group_enter(picturesGroup)

let key = attendee.objectID

let url = NSURL(string: "https://graph.facebook.com/\(key)/picture?type=large")

let urlRequest = NSURLRequest(URL: url!)

//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}

// Display the image
let image = UIImage(data: data)
if(image != nil){
attendee.image = image
}

dispatch_group_leave(picturesGroup)
}
}

dispatch_group_notify(picturesGroup, dispatch_get_main_queue()) {
completion(true, attendees: attendees)
}
}

func setupEvents(completion: (result: Bool, Event: Event) -> Void){

// get event info and then for each event...

getAttendees(child:snapshot.key, completion: { (result, attendeesReturned) in
if result {
self.getAttendeesPictures(attendees: attendeesReturned, completion: { (result, attendees) in

// do something with completed array and attendees


}
}
else {

}
})

}

The above code is just an outline, but hopefully points you in the right direction.

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

Ensuring Async Action Has Completed in Firebase

One approach to signal completion of an asynchronous function is using a completion handler. You already used completion handlers in the Firebase API and there are many APIs in the system frameworks, so I don't explain that further.

Given this approach, wrap your code into a function, say updateContacts with a completion handler. Usually an asynchronous function returns the computed value or an error. In some cases, it just succeeds or fails - without returning a value. You express this in the signature of the completion handler. Your function updateContacts may not compute a value, but it may fail or succeed anyway. Then, you can use an optional error: if it is nil, the task succeeded, otherwise it contains the error that occurred.

When your underlying task has been completed, call the completion handler with the result.

Note: You must ensure, that the completion handler will be eventually called!

func updateContacts(completion: (ErrorType?)-> ()) {
friendsURL.observeSingleEventOfType(.Value, withBlock: { snapshot in
...
...
...
usersURL.observeSingleEventOfType(.Value, withBlock: { snapshot in
...
let error = // nil or an error
completion(error)
return
}
completion(nil)
}
}

Now, when you have an array of asynchronous subtasks, that will be called in parallel and you want to signal completion of updateContacts when all subtasks have been completed - you can utilise dispatch groups:

let group = dispatch_group_create()
var error: ErrorType?
contactNumbers.forEach { number in
dispatch_group_enter(group)
queryAsync(number) { (result, error) in
if let error = error {
// handle error
} else {
...
}
dispatch_group_leave(group)
}
}
dispatch_group_notify(group, queue) {
// call the completion handler of your function `updateContacts`:
completion(error)
}

Swift- Waiting for asynchronous for-in loop to complete before calling completion handler swift

A couple of thoughts:

  1. Avoid ever calling wait from the main thread. The use cases for that are pretty limited. The notify is a much safer way to achieve the same thing.

  2. Make sure you call leave from every path inside you’re loop. This can be achieved nicely with defer block.

So:

func foo(completion: @escaping (Double?) -> Void) {
ref.observeSingleEvent(of: .value) { snapshot in
guard let bills = snapshot.value as? [String: AnyObject] else {
//error
completion(nil)
return
}

let group = DispatchGroup()
var runningTotal = 0.0

for billId in bills.keys {
group.enter()
print("Entering")
Database.database().reference().child("bills").child(billId).observeSingleEvent(of: .value) { snapshot in
defer { group.leave() }
guard let bill = snapshot.value as? [String: AnyObject] else {
return
}
if let amount = bill["amount"] as? Double {
runningTotal += amount
}
print("Leaving")
}
}
group.notify(queue: .main) {
completion(runningTotal)
}
}
}


Related Topics



Leave a reply



Submit