Firebase & Swift: Asynchronous calls, Completion Handlers - ios

I have read up a lot on this subject but have still been stumped on this specific problem. I have many Firebase calls that rely on each other. This is a kind of simplified example of my code. I had trouble making it any shorter and still getting the point across:
class ScoreUpdater {
static let ref = Database.database().reference()
var userAranking = Int?
var userBranking = Int?
var rankingAreceived = false
var rankingBreceived = false
var sum = 0
// Pass in the current user and the current meme
static func beginUpdate(memeID: String, userID: String) {
// Iterate through each user who has ranked the meme before
ScoreUpdater.ref.child("memes/\(memeID)/rankings")observeSingleEvent(of: .value) {
let enumerator = snapshot.children
while let nextUser = enumerator.nextObject() as? DataSnapshot {
// Create a currentUpdater instance for the current user paired with each other user
let currentUpdater = ScoreUpdater()
This is where the asynchronous calls start. Multiple gatherRankingValues functions can run at one time. This function contains a Firebase call which is asynchronous, which is okay for this function. The updateScores however cannot run until gatherRankingValues is finished. That is why I have the completion handler. I think this area is okay based on my debug printing.
// After gatherRankingValues is finished running,
// then updateScores can run
currentUpdater.gatherRankingValues(userA: userID, userB: nextUser.key as! String) {
currentUpdater, userA, userB in
currentUpdater.updateScores(userA: userA, userB:userB)
}
}
}
}
func gatherRankingValues(userA: String, userB: String, completion: #escaping (_ currentUpdater: SimilarityScoreUpdater, _ userA: String, _ userB: String) -> Void) {
// Iterate through every meme in the database
ScoreUpdater.ref.child("memes").observeSingleEvent(of: .value) {
snapshot in
let enumerator = snapshot.children
while let nextMeme = enumerator.nextObject() as? DataSnapshot {
Here is where the main problem comes in. The self.getRankingA and self.getRankingB never run. Both of these methods need to run before the calculation method. I try to put in the "while rankingReceived == false" loop to keep the calculation from starting. I use the completion handler to notify within the self.rankingAreceived and self.rankingBreceived when the values have been received from the database. Instead, the calculation never happens and the loop becomes infinite.
If I remove the while loop waiting for the rankings to be received, the calculations will be "carried out" except the end result ends up being nil because the getRankingA and getRankingB methods still do not get called.
self.getRankingA(userA: userA, memeID: nextMeme.key) {
self.rankingAreceived = true
}
self.getRankingB(userB: userB, memeID: nextMeme.key) {
self.rankingBreceived = true
}
while self.rankingAreceived == false || self.rankingBreceived == false {
continue
}
self.calculation()
}
So yes, every meme gets looped through before the completion is called, but the rankings don't get called. I can't figure out how to get the loop to wait for the rankings from getRankingA and getRankingB and for the calculation method to run before continuing on to the next meme. I need completion of gatherRankingValues (see below) to be called after the loop has been through all the memes, but each ranking and calculation to complete also before the loop gets called again ... How can I within the getRankingA and getRankingB completion handlers tell the meme iterating loop to wait up?
// After every meme has been looped through for this pair of users, call completion
completion(self, userA, userB)
}
}
function getRankingA(userA: String, memeID: String, completion: #escaping () -> Void) {
ScoreUpdater.ref.child("memes/\(memeID)\rankings\(userA)").observeSingleEvent(of: .value) {
snapshot in
self.userAranking = snapshot.value
completion()
}
}
function getRankingB(userB: String, memeID: String, completion: #escaping () -> Void) {
ScoreUpdater.ref.child("memes/\(memeID)\rankings\(userB)").observeSingleEvent(of: .value) {
snapshot in
self.userBranking = snapshot.value
completion()
}
}
func calculation() {
self.sum = self.userAranking + self.userBranking
self.userAranking = nil
self.userBranking = nil
}
func updateScores() {
ScoreUpdater.ref.child(...)...setValue(self.sum)
}
}

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!

To wait for a loop to complete or better, do stuff after some async call is executed, you could use DispatchGroups. This example shows how they work:
let group = DispatchGroup()
var isExecutedOne = false
var isExecutedTwo = false
group.enter()
myAsyncCallOne() {
isExecutedOne = true
group.leave()
}
group.enter()
myAsyncCallTwo() {
isExecutedOTwo = true
group.leave()
}
group.notify(queue: .main) {
if isExecutedOne && isExecutedTwo {
print("hooray!")
} else {
print("nope...")
}
}
UPDATE
This example shows you how the group is used to control the output. There is no need to wait() or something. You just enter the group in every iteration of the loop, leave it in the async callbacks and when every task left the group, group.notify()is called and you can do the calculations:
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()
}
}
group.notify()is called when all the calls have left the group. You can have nested groups too.
Happy Coding!

Related

Firebase function not getting called inside a forEach loop with a DispatchGroup

I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}

App freeze with DispatchSemaphore wait()

I have created a function getFriends that reads a User's friendlist from firestore and puts each friend in a LocalUser object (which is my custom user class) in order to display the friendlist in a tableview. I need the DispatchSemaphore.wait() because I need the for loop to iterate only when the completion handler inside the for loop is called.
When loading the view, the app freezes. I know that the problem is that semaphore.wait() is called in the main thread. However, from reading DispatchQueue-tutorials I still don't understand how to fix this in my case.
Also: do you see any easier ways to implement what I want to do?
This is my call to the function in viewDidLoad():
self.getFriends() { (friends) in
self.foundFriends = friends
self.friendsTable.reloadData()
}
And the function getFriends:
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
}
}
friendsUID is a dict with each friend's uid as a key and true as the value. Since I only need the keys, I store them in the array friendsIdents. Function getUser searches the passed uid in firestore and creates the corresponding LocalUser (usr). This finally gets appended in friends array.
You should almost never have a semaphore.wait() on the main thread. Unless you expect to wait for < 10ms.
Instead, consider dispatching your friends list processing to a background thread. The background thread can perform the dispatch to your database/api and wait() without blocking the main thread.
Just make sure to use DispatchQueue.main.async {} from that thread if you need to trigger any UI work.
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
DispatchQueue.global(qos: .userInitiated).async {
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
// Insert here a DispatchQueue.main.async {} if you need something to happen
// on the main queue after you are done processing all entries
}
}

Completion Block Issues

Okay so I have a potential problem that I need help/advice on. I have two functions that do the job of pulling keys from my database and then passing those keys to another function which ultimately grabs all the data.
This is the first function
static func showEvent(for currentLocation: CLLocation,completion: #escaping ([Event]) -> Void) {
//getting firebase root directory
var currentEvents = [Event]()
var geoFireRef: DatabaseReference?
var geoFire:GeoFire?
geoFireRef = Database.database().reference().child("eventsbylocation")
geoFire = GeoFire(firebaseRef: geoFireRef)
let circleQuery = geoFire?.query(at: currentLocation, withRadius: 10.0)
circleQuery?.observe(.keyEntered, with: { (key: String!, location: CLLocation!) in
print("Key '\(key)' entered the search area and is at location '\(location)'")
EventService.show(forEventKey: key, completion: { (event) in
currentEvents.append(event!)
completion(currentEvents)
})
})
}
This function uses the EventService.show function to ultimately grab the data like mentioned before.
static func show(forEventKey eventKey: String, completion: #escaping (Event?) -> Void) {
// print(eventKey)
let ref = Database.database().reference().child("events").child(eventKey)
print(eventKey)
//pull everything
ref.observeSingleEvent(of: .value, andPreviousSiblingKeyWith: { (snapshot,eventKey) in
//print(snapshot.value ?? "")
guard let event = Event(snapshot: snapshot) else {
return completion(nil)
}
completion(event)
})
}
When both of these functions are done it returns back to my main function with the array of events to help populate my collectionView.
This is that function
#objc func grabUserLoc(){
LocationService.getUserLocation { (location) in
guard let currentLocation = location else {
return
}
PostService.showEvent(for: currentLocation, completion: { (events) in
self.allEvents = events
print("Event count in PostService Closure:\(self.allEvents.count)")
self.dynamoCollectionView.reloadData()
}
)
print("Latitude: \(currentLocation.coordinate.latitude)")
print("Longitude: \(currentLocation.coordinate.longitude)")
}
}
Now upon tracing the stack I see that reloadData is called multiple times. Is there any way that I can go about fixing these functions to actually do the completion block when all the data is pulled. So that reloadData is called only once not every single time an instance of an event comes back?
I hope my question makes sense
DispatchGroups were brought to my attention could anyone maybe show me an implementation of an answer
Taken from the article in my comment:
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
longRunningFunction { dispatchGroup.leave() }
dispatchGroup.enter()
longRunningFunctionTwo { dispatchGroup.leave() }
dispatchGroup.notify(queue: .main) {
print("Both functions complete 👍")
}
You create a group, in each Async activity you enter the group, when that Async activity finishes you leave the group, when the dispatch group leaves as many things as it enters it will call the notify.
EDIT:
So in your case you can either pass the group to the funcs, to leave when they are done or have each func have an optional completion which then is called back to your original to leave the group.

Fetch data from Firebase by joining tables in iOS

I am trying to fetch data from two different Firebase tables. Here is the structure of table:
Post {
1{
pImages{
i1:true
i2:true
}
}
2{
pImages{
i3:true
}
}
}
Images{
i1{
iUrl : ....
pId : 1
}
i2{
iUrl :...
pId : 1
}
i3{
iUrl:....
pId : 2
}
}
I need to retrieve images corresponding to post with id = 1. The following is my implementation to retrieve images:
func retrieveImagesForPost(postId: String,completion: (result: AnyObject?, error: NSError?)->()){
var imgArray:[Image]=[]
let postsRef = self.ref.child("post")
let imagesRef = self.ref.child("image")
let postImagesRef = postsRef.child(postId).child("pImages");
postImagesRef.observeEventType(FIRDataEventType.Value, withBlock: { (snapshot) in
for item in snapshot.children{
imagesRef.child(item.key).observeSingleEventOfType(.Value, withBlock: { (snap) in
let image = Image(snapshot: snap)
print(image)
imgArray.append(image)
})
}
print(snapshot.key)
print("called")
completion(result:imgArray, error:nil)
})
}
But, the problem is I am not able to get all images in imgArray to be able to send to completion handler. Below is the output of calling retrieveImagesForPost with post id ==1.
pImages
called
<TestProject.Image: 0x7f9551e82000>
<TestProject.Image: 0x7f955466a150>
The images are retrieved after the completion handler is called. I tried the dispatch groups and the semaphores approach as described in the following post. But the results are still the same. How can I make completion handler to wait for all images to be fetched from Firebase?
Keep a counter that you increase as each image is loaded. Once the counter reaches the length of the snapshot.children list, you're done and call your completion handler.
let postImagesRef = postsRef.child(postId).child("pImages");
postImagesRef.observeEventType(FIRDataEventType.Value, withBlock: { (snapshot) in
var counter = 0
for item in snapshot.children{
imagesRef.child(item.key).observeSingleEventOfType(.Value, withBlock: { (snap) in
let image = Image(snapshot: snap)
print(image)
imgArray.append(image)
counter = counter + 1
if (counter == snapshot.childrenCount) {
completion(result:imgArray, error:nil)
}
})
}
})
You should probably add some error handling in the above, but in general this approach is tried and tested.
Another answer for this problem is to use GCD's DispatchGroup.
First you need to create a dispatch group with DispatchGroup. In this case, you need to manually tell the group when work is being started with enter() and when it's finished with leave(). Then the dispatch group's notify(queue:execute:) will execute the completion handler on the main queue.
Be careful! The number of enters and leaves must be balanced or the dispatch group's notify will never be called.
let dispatchGroup = DispatchGroup()
let postImagesRef = postsRef.child(postId).child("pImages");
postImagesRef.observeEventType(FIRDataEventType.value, withBlock: { (snapshot) in
for item in snapshot.children{
dispatchGroup.enter()
imagesRef.child(item.key).observeSingleEventOfType(.value, withBlock: { (snap) in
let image = Image(snapshot: snap)
print(image)
imgArray.append(image)
dispatchGroup.leave()
})
}
})
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
completion(result: imgArray)
})

Ensuring Async Action Has Completed in Firebase

I need to run concurrent queries to Firebase in swift. How can I ensure that a looped query has finished before another action is allowed to start in my app?
For example, the first query is a straightforward, and simply pulls data. but the second, iterates through an array looking for a certain node in my firebase database, looping through the array self.contacts:
// First check if friend, and remove/add to
friendsURL.observeSingleEventOfType(.Value, withBlock: { snapshot in
for oneSnapshot in snapshot.children {
for oneContact in contactsArray {
for oneContactPhoneNum in oneContact.phoneNumbers {
let phoneNumber = oneContactPhoneNum.value as! CNPhoneNumber
contactNumber = phoneNumber.stringValue
// Clean the number
let stringArray = contactNumber!.componentsSeparatedByCharactersInSet(
NSCharacterSet.decimalDigitCharacterSet().invertedSet)
let newString = "1" + stringArray.joinWithSeparator("")
let firebaseFriendNumber = oneSnapshot.value["phoneNumber"] as! String
if newString == firebaseFriendNumber {
self.friends.append(Friend(userName: oneSnapshot.value["userName"] as! String,phoneNumber: firebaseFriendNumber, status: 2, name: oneContact.givenName, userID: oneSnapshot.key))
// Remove that contact
self.contacts.removeObject(oneContact)
}
}
}
}
// Now do the users search:
for oneContact in self.contacts {
for oneContactNumer in oneContact.phoneNumbers {
let phoneNumber = oneContactNumer.value as! CNPhoneNumber
contactNumber = phoneNumber.stringValue
let stringArray = contactNumber!.componentsSeparatedByCharactersInSet(
NSCharacterSet.decimalDigitCharacterSet().invertedSet)
let newString = "1" + stringArray.joinWithSeparator("")
let usersURL: Firebase! = Firebase(url: firebaseMainURL + "presentUserIDUserNameByPhoneNumber/" + newString)
// Check db:
usersURL.observeSingleEventOfType(.Value, withBlock: { snapshot in
if snapshot.childrenCount > 1 {
// They are users (but not your friends):
self.friends.append(Friend(userName: snapshot.value["userName"] as! String, phoneNumber: snapshot.key, status: 1, name: "test", userID: snapshot.value["userID"] as! String))
let userName = snapshot.value["userName"] as! String
print("Friends name: " + userName)
// Remove that contact
self.contacts.removeObject(oneContact)
}
})
}
}
})
How can I test and check when the second, on usersURL, has completed before allowing other actions to occur in app?
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)
}

Resources