Firebase & Swift: Asynchronous calls, Completion H

2019-08-21 07:29发布

问题:

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

}

回答1:

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!



回答2:

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!