Swift 3 DispatchGroup single success DispatchWorkItem - ios

Here is the deal. I'm attempting to walk the tree. But want to do so concurrently. So each time i walk onto a node i need to concurrently walk all of it's nodes and so on.
But. I do not want to wait for the whole DispatchGroup to finish to get results since it's like a worst case scenario in Big O.
Instead i want to cancel all the other DispatchWorkItems and leave group for them in the successing one. Tried to do so with counting the task which ended. Obviously i'm doing something wrong or misunderstand how to use this.
The code below was written just for purpose of example and to test the idea.
Consider the real world situation is that in the DispatchWorkItem you can call recursively another handle function of a current node of tree.
func handle(completion: #escaping (Int) -> Void) {
var result: Int = 0
var count = 7
let group = DispatchGroup()
let queue = DispatchQueue(label: "q", attributes: .concurrent)
var items = [DispatchWorkItem]()
let item1 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...1000 { continue }
count -= 1
group.leave()
print("left 1")
}
let item2 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...2000 { continue }
count -= 1
group.leave()
print("left 2")
}
let item3 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...6000 { continue }
count -= 1
group.leave()
print("left 3")
}
let item4 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...3000 { continue }
result = 42
items.forEach { $0.cancel() }
for _ in 0..<count {
group.leave()
}
print("ok; left 4")
}
let item5 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...50000 { continue }
count -= 1
group.leave()
print("left 5")
}
let item6 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...6000 { continue }
count -= 1
group.leave()
print("left 6")
}
let item7 = DispatchWorkItem(flags: .inheritQoS) {
for _ in 0...8000 { continue }
count -= 1
group.leave()
print("left 7")
}
items.append(item1)
items.append(item2)
items.append(item3)
items.append(item4)
items.append(item5)
items.append(item6)
items.append(item7)
for item in items {
group.enter()
queue.async(execute: item)
}
group.notify(queue: queue) {
return
}
}
test() {
handle { result in
print(result)
}
}

You can't be reading from and writing to your count variable from multiple threads at once. You need to put a mutex lock on the count. You have an unstable situation trying to access and/or change count from multiple threads. Also, you should design this to not need counting at all.

A couple of thoughts:
If you want to cancel a time consuming task, you need to periodically check isCancelled. See https://stackoverflow.com/a/38372384/1271826.
If you are going to update count or items from multiple threads, you have to synchronize that interaction (e.g. with a lock or dedicated serial queue). Int and Array are not thread-safe, so you'll have to manage that yourself.
Keeping track of count and using DispatchGroup and keeping track of your own collection of DispatchWorkItem references is going to take a little work. Operation queues get your out of all of that. That having been said, if maximum efficiency is required, then perhaps you want to stay within dispatch queues, but it's just a lot more work.

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

How to wait for an API request to finish before storing data from function callback?

In a personal project of mine, I have created an API caller to retrieve a user's saved tracks from the Spotify API. The Spotify endpoint which I am using has a limit (maximum of 50 tracks per request) as well as an offset (starting index of first track in request), which is why I decided to use a FOR loop to get a series of track pages (each 50 tracks) and append them to a global array. The data is loaded from the main thread, and while the data is being requested, I display a child view controller with a spinner view. Once the data request has completed, I remove the spinner view, and transition to another view controller (passing the data as a property).
I have tried many things, but the array of tracks is always empty following the API request. I have a feeling it has to do with the synchronicity of my request, or maybe its possible that I'm not handling it correctly. Ideally, I would like to wait until the request from my API finishes, then append the result to the array. Do you have any suggestions on how I could solve this? Any help is much appreciated!
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
DispatchQueue.main.async { [weak self] in
if (self?.dropdownButton.dropdownLabel.text == "My saved music") {
self?.fetchSavedMusic() { tracksArray in
self?.tracksArray = tracksArray
}
}
...
self?.remove(asChildViewController: loadViewController)
self?.navigateToFilterScreen(tracksArray: self!.tracksArray)
}
}
private func fetchSavedMusic(completion: #escaping ([Tracks]) -> ()) {
let limit = 50
var offset = 0
var total = 200
for _ in stride(from: 0, to: total, by: limit) {
getSavedTracks(limit: limit, offset: offset) { tracks in
//total = tracks.total
self.tracksArray.append(tracks)
}
print(offset, limit)
offset = offset + 50
}
completion(tracksArray)
}
private func getSavedTracks(limit: Int, offset: Int, completion: #escaping (Tracks) -> ()) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { (result) in
switch result {
case .success(let model):
completion(model)
print("success")
case .failure(let error):
print("Error retrieving saved tracks: \(error.localizedDescription)")
print(error)
}
}
}
private func navigateToFilterScreen(tracksArray: [Tracks]) {
let vc = FilterViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
vc.paginatedTracks = tracksArray
show(vc, sender: self)
}
First you need to call completion when all of your data is loaded. In your case you call completion(tracksArray) before any of the getSavedTracks return.
For this part I suggest you to recursively accumulate tracks by going through all pages. There are multiple better tools to do so but I will give a raw example of it:
class TracksModel {
static func fetchAllPages(completion: #escaping ((_ tracks: [Track]?) -> Void)) {
var offset: Int = 0
let limit: Int = 50
var allTracks: [Track] = []
func appendPage() {
fetchSavedMusicPage(offset: offset, limit: limit) { tracks in
guard let tracks = tracks else {
completion(allTracks) // Most likely an error should be handled here
return
}
if tracks.count < limit {
// This was the last page because we got less than limit (50) tracks
completion(allTracks+tracks)
} else {
// Expecting another page to be loaded
offset += limit // Next page
allTracks += tracks
appendPage() // Recursively call for next page
}
}
}
appendPage() // Load first page
}
private static func fetchSavedMusicPage(offset: Int, limit: Int, completion: #escaping ((_ tracks: [Track]?) -> Void)) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { result in
switch result {
case .success(let model):
completion(model)
case .failure(let error):
print(error)
completion(nil) // Error also needs to call a completion
}
}
}
}
I hope comments will clear some things out. But the point being is that I nested an appendPage function which is called recursively until server stops sending data. In the end either an error occurs or the last page returns fewer tracks than provided limit.
Naturally it would be nicer to also forward an error but I did not include it for simplicity.
In any case you can now anywhere TracksModel.fetchAllPages { } and receive all tracks.
When you load and show your data (createSpinnerView) you also need to wait for data to be received before continuing. For instance:
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
TracksModel.fetchAllPages { tracks in
DispatchQueue.main.async {
self.tracksArray = tracks
self.remove(asChildViewController: loadViewController)
self.navigateToFilterScreen(tracksArray: tracks)
}
}
}
A few components may have been removed but I hope you see the point. The method should be called on main thread already. But you are unsure what thread the API call returned on. So you need to use DispatchQueue.main.async within the completion closure, not outside of it. And also call to navigate within this closure because this is when things are actually complete.
Adding situation for fixed number of requests
For fixed number of requests you can do all your requests in parallel. You already did that in your code.
The biggest problem is that you can not guarantee that responses will come back in same order than your requests started. For instance if you perform two request A and B it can easily happen due to networking or any other reason that B will return before A. So you need to be a bit more sneaky. Look at the following code:
private func loadPage(pageIndex: Int, perPage: Int, completion: #escaping ((_ items: [Any]?, _ error: Error?) -> Void)) {
// TODO: logic here to return a page from server
completion(nil, nil)
}
func load(maximumNumberOfItems: Int, perPage: Int, completion: #escaping ((_ items: [Any], _ error: Error?) -> Void)) {
let pageStartIndicesToRetrieve: [Int] = {
var startIndex = 0
var toReturn: [Int] = []
while startIndex < maximumNumberOfItems {
toReturn.append(startIndex)
startIndex += perPage
}
return toReturn
}()
guard pageStartIndicesToRetrieve.isEmpty == false else {
// This happens if maximumNumberOfItems == 0
completion([], nil)
return
}
enum Response {
case success(items: [Any])
case failure(error: Error)
}
// Doing requests in parallel
// Note that responses may return in any order time-wise (we can not say that first page will come first, maybe the order will be [2, 1, 5, 3...])
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count) { // Start with all nil
didSet {
// Yes, Swift can do this :D How amazing!
guard responses.contains(where: { $0 == nil }) == false else {
// Still waiting for others to complete
return
}
let aggregatedResponse: (items: [Any], errors: [Error]) = responses.reduce((items: [], errors: [])) { partialResult, response in
switch response {
case .failure(let error): return (partialResult.items, partialResult.errors + [error])
case .success(let items): return (partialResult.items + [items], partialResult.errors)
case .none: return (partialResult.items, partialResult.errors)
}
}
let error: Error? = {
let errors = aggregatedResponse.errors
if errors.isEmpty {
return nil // No error
} else {
// There was an error.
return NSError(domain: "Something more meaningful", code: 500, userInfo: ["all_errors": errors]) // Or whatever you wish. Perhaps just "errors.first!"
}
}()
completion(aggregatedResponse.items, error)
}
}
pageStartIndicesToRetrieve.enumerated().forEach { requestIndex, startIndex in
loadPage(pageIndex: requestIndex, perPage: perPage) { items, error in
responses[requestIndex] = {
if let error = error {
return .failure(error: error)
} else {
return .success(items: items ?? [])
}
}()
}
}
}
The first method is not interesting. It just loads a single page. The second method now collects all the data.
First thing that happens is we calculate all possible requests. We need a start index and per-page. So the pageStartIndicesToRetrieve for case of 145 items using 50 per page will return [0, 50, 100]. (I later found out we only need count 3 in this case but that depends on the API, so let's stick with it). We expect 3 requests starting with item indices [0, 50, 100].
Next we create placeholders for our responses using
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count)
for our example of 145 items and using 50 per page this means it creates an array as [nil, nil, nil]. And when all of the values in this array turn to not-nil then all requests have returned and we can process all of the data. This is done by overriding the setter didSet for a local variable. I hope the content of it speaks for itself.
Now all that is left is to execute all requests at once and fill the array. Everything else should just resolve by itself.
The code is not the easiest and again; there are tools that can make things much easier. But for academical purposes I hope this approach explains what needs to be done to accomplish your task correctly.

How to force firebase functions to execute before continuing swift

I am trying to query data from firebase inside a for loop, my problem is since the queries take time to connect, swift is jumping over the queries and coming back later to do them. This creates the problem where my loop counter is ticking up but the queries are being saved for later, when the queries finally do get executed, the counter variable is all out of wack.
Where the code is being skipped is right after the query, where I am trying to append to an array.
func getSelectedData() {
var exerciseIndex = 0
for i in 0...Master.exercises.count - 1 {
if Master.exercises[i].name == self.exerciseName {
exerciseIndex = i
}
}
let numOfSets = Master.exercises[exerciseIndex].totalSets
// For each date record
for count in 0...self.returnedExercises.count-1 {
// Creates a new dataSet
dataSet.append(dataSetStruct())
dataSet[count].date = returnedExercises[count]
for number in 0...(numOfSets - 1) {
// Retrives the reps
let repsDbCallHistory = db.collection("users").document("\(userId)").collection("ExerciseData").document("AllExercises").collection(exerciseName).document(returnedExercises[count]).collection("Set\(number + 1)").document("reps")
repsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].repsArray.append(data["Reps\(number + 1)"] as! Int)
}
else {
// error
}
}
//Retrives the weights
let weightsDbCallHistory = db.collection("users").document("\(userId)").collection("ExerciseData").document("AllExercises").collection(exerciseName).document(returnedExercises[count]).collection("Set\(number + 1)").document("weights")
weightsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].weightsArray.append(data["Weight\(number + 1)"] as! Float)
self.updateGraph()
}
else {
// error
}
}
}
}
}
I even tried breaking out the query into another function but this doesn't seem to fix the issue.
Any help is appreciated, thanks.
EDIT:
func getSelectedData() {
if returnedExercises.count > 0 {
// Create a dispatch group
let group = DispatchGroup()
print("Getting Data")
// For each date record
for count in 0...self.returnedExercises.count-1 {
// Creates a new dataSet
self.dataSet.append(dataSetStruct())
self.dataSet[count].date = self.returnedExercises[count]
for number in 0...(self.numOfSets - 1) {
print("At record \(count), set \(number)")
// Enter the group
group.enter()
// Start the dispatch
DispatchQueue.global().async {
// Retrives the reps
let repsDbCallHistory = self.db.collection("users").document("\(self.userId)").collection("ExerciseData").document("AllExercises").collection(self.exerciseName).document(self.returnedExercises[count]).collection("Set\(number + 1)").document("reps")
repsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].repsArray.append(data["Reps\(number + 1)"] as! Int)
print("Getting data: \(number)")
group.leave()
}
else {
// error
}
}
}
group.wait()
print("Finished getting data")
}
}
I tried to simplify the function for now and only have one database call in the function to try the dispatch groups. I am not sure why firebase is doing this but the code never executes the group.leave, the program just sits idle. If I am doing something wrong please let me know, thanks.
This is what the print statements are showing:
Getting Data
At record 0, set 0
At record 0, set 1
At record 0, set 2
At record 1, set 0
At record 1, set 1
At record 1, set 2
print("Getting data: (number)") is never being executed for some reason.
I am thinking that maybe firebase calls are ran on a separate thread or something, which would made them pause execution as well, but that's just my theory
EDIT2::
func getOneRepMax(completion: #escaping (_ message: String) -> Void) {
if returnedOneRepMax.count > 0 {
print("Getting Data")
// For each date record
for count in 0...self.returnedOneRepMax.count-1 {
// Creates a new dataSet
oneRPDataSet.append(oneRepMaxStruct())
oneRPDataSet[count].date = returnedOneRepMax[count]
// Retrives the reps
let oneRepMax = db.collection("users").document("\(userId)").collection("UserInputData").document("OneRepMax").collection(exerciseName).document(returnedOneRepMax[count])
oneRepMax.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.oneRPDataSet[count].weight = Float(data["Weight"] as! String)!
print("Getting data: \(count)")
completion("DONE")
self.updateGraph()
}
else {
// error
}
}
}
}
}
I tried using completion handlers for a different function and it is also not working properly.
self.getOneRepMax(completion: { message in
print(message)
})
print("Finished getting data")
The order that the print statements should go:
Getting Data
Getting data: 0
Done
Getting data: 1
Done
Finished getting data
The order that the print statements are coming out right now:
Getting Data
Finished getting data
Getting data: 1
Done
Getting data: 0
Done
I am not even sure how it is possible that the count is backwards since my for loop counts up, what mistake am I making?
I think what you need are Dispatch Groups.
let dispatchGroup1 = DispatchGroup()
let dispatchGroup2 = DispatchGroup()
dispatchGroup1.enter()
firebaseRequest1() { (_, _) in
doThings()
dispatchGroup1.leave()
}
dispatchGroup2.enter()
dispatchGroup1.notify(queue: .main) {
firebaseRequest2() { (_, _ ) in
doThings()
dispatchGroup2.leave()
}
dispatchGroup2.notify(queue: .main) {
completionHandler()
}

Wait for DispatchQueue [duplicate]

How could I make my code wait until the task in DispatchQueue finishes? Does it need any CompletionHandler or something?
func myFunction() {
var a: Int?
DispatchQueue.main.async {
var b: Int = 3
a = b
}
// wait until the task finishes, then print
print(a) // - this will contain nil, of course, because it
// will execute before the code above
}
I'm using Xcode 8.2 and writing in Swift 3.
If you need to hide the asynchronous nature of myFunction from the caller, use DispatchGroups to achieve this. Otherwise, use a completion block. Find samples for both below.
DispatchGroup Sample
You can either get notified when the group's enter() and leave() calls are balanced:
func myFunction() {
var a = 0
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
a = 1
group.leave()
}
// does not wait. But the code in notify() is executed
// after enter() and leave() calls are balanced
group.notify(queue: .main) {
print(a)
}
}
or you can wait:
func myFunction() {
var a = 0
let group = DispatchGroup()
group.enter()
// avoid deadlocks by not using .main queue here
DispatchQueue.global(qos: .default).async {
a = 1
group.leave()
}
// wait ...
group.wait()
print(a) // you could also `return a` here
}
Note: group.wait() blocks the current queue (probably the main queue in your case), so you have to dispatch.async on another queue (like in the above sample code) to avoid a deadlock.
Completion Block Sample
func myFunction(completion: #escaping (Int)->()) {
var a = 0
DispatchQueue.main.async {
let b: Int = 1
a = b
completion(a) // call completion after you have the result
}
}
// on caller side:
myFunction { result in
print("result: \(result)")
}
In Swift 3, there is no need for completion handler when DispatchQueue finishes one task.
Furthermore you can achieve your goal in different ways
One way is this:
var a: Int?
let queue = DispatchQueue(label: "com.app.queue")
queue.sync {
for i in 0..<10 {
print("Ⓜ️" , i)
a = i
}
}
print("After Queue \(a)")
It will wait until the loop finishes but in this case your main thread will block.
You can also do the same thing like this:
let myGroup = DispatchGroup()
myGroup.enter()
//// Do your task
myGroup.leave() //// When your task completes
myGroup.notify(queue: DispatchQueue.main) {
////// do your remaining work
}
One last thing: If you want to use completionHandler when your task completes using DispatchQueue, you can use DispatchWorkItem.
Here is an example how to use DispatchWorkItem:
let workItem = DispatchWorkItem {
// Do something
}
let queue = DispatchQueue.global()
queue.async {
workItem.perform()
}
workItem.notify(queue: DispatchQueue.main) {
// Here you can notify you Main thread
}
Swift 5 version of the solution
func myCriticalFunction() {
var value1: String?
var value2: String?
let group = DispatchGroup()
group.enter()
//async operation 1
DispatchQueue.global(qos: .default).async {
// Network calls or some other async task
value1 = //out of async task
group.leave()
}
group.enter()
//async operation 2
DispatchQueue.global(qos: .default).async {
// Network calls or some other async task
value2 = //out of async task
group.leave()
}
group.wait()
print("Value1 \(value1) , Value2 \(value2)")
}
Use dispatch group
dispatchGroup.enter()
FirstOperation(completion: { _ in
dispatchGroup.leave()
})
dispatchGroup.enter()
SecondOperation(completion: { _ in
dispatchGroup.leave()
})
dispatchGroup.wait() // Waits here on this thread until the two operations complete executing.
In Swift 5.5+ you can take advantage of Swift Concurrency which allows to return a value from a closure dispatched to the main thread
func myFunction() async {
var a : Int?
a = await MainActor.run {
let b = 3
return b
}
print(a)
}
Task {
await myFunction()
}
Swift 4
You can use Async Function for these situations. When you use DispatchGroup(),Sometimes deadlock may be occures.
var a: Int?
#objc func myFunction(completion:#escaping (Bool) -> () ) {
DispatchQueue.main.async {
let b: Int = 3
a = b
completion(true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
myFunction { (status) in
if status {
print(self.a!)
}
}
}
Somehow the dispatchGroup enter() and leave() commands above didn't work for my case.
Using sleep(5) in a while loop on the background thread worked for me though. Leaving here in case it helps someone else and it didn't interfere with my other threads.

Firebase & Swift: Asynchronous calls, Completion Handlers

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!

Resources