I'm learning swift programming and i built a music trivia app for iOS that use firebase real time database. I have a function that before the game start get the number of record for each category of the game and update a class variable that is a tuple called records.
var records = (classical: 0, mix: 0, casual: 0)
private func getNumberOfRecords(category: String) {
var ref: DatabaseReference!
ref = Database.database().reference().child(category)
ref.observeSingleEvent(of: .value) { snapshot in
let enumerator = snapshot.children
while((enumerator.nextObject() as? DataSnapshot) != nil) {
switch category {
case "casual":
records.casual += 1
case "classical":
records.classical += 1
case "mix":
records.mix += 1
default: print("Error loading record from category \(category)")
}
}
}
}
Here is how I call the function and because it's asynchronous the response I receive from firebase and I need to know the number of records before call another function that will load the game data from the database i use DispatchQueue and wait 3 seconds.
getNumberOfRecords(category: "classical")
getNumberOfRecords(category: "casual")
getNumberOfRecords(category: "mix")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
loadDataFromDB(records: records.classical, category: "classical")
loadDataFromDB(records: records.casual, category: "casual")
loadDataFromDB(records: records.mix, category: "mix")
}
Now this is working but it's pretty ugly, especially using the function getNumberOfRecords to update the global tuple records. I don't know how can I rewrite this because if i make getNumberOfRecords return a tuple rather than update the global one is always 0 when i pass it as parameter of the function loadDataFromDB inside the DispatchQueue.
Thanks in advance to anyone will have time to read this.
1 Answer 1
A typical way to write an asynchronous function is to have it take a function as an argument that will be called with the results when they are ready. Such a function is often called a completion handler.
private func getNumberOfRecords(category: String, completion: (Int) -> Void) {
...
default: print("Error loading record from category \(category)")
}
}
}
completion(records.count)
}
Then at the call site you can write:
getNumberOfRecords(category: "classical", completion: { count in ... })
or better written with trailing-closure syntax:
getNumberOfRecords(category: "classical") { count in ... }
You still have the problem of three asynchronous things, and instead of chaining them together, you can also avoid looping over the data 3 times, you can loop over the data once and remember the counts for each item, using a dictionary:
private func getNumberOfRecords(completion: ([String: Int] -> Void)) {
var categoryCounts: [String: Int] = [:]
{
... loop over the data here and instead of switch category ...
categoryCounts[category] = (categoryCounts[category] ?? 0) + 1
}
completion(categoryCounts)
}
getNumberOfRecords { categoryCounts in
for (category, count) in categoryCounts {
loadDataFromDB(records: count, category: category)
}
}
-
\$\begingroup\$ Thank you! the completion handler is exactly what I was looking for. I was trying to solve this using promises but I think it's more important to learn how to write an asynchronous function without any external library first. Again thanks for your time! \$\endgroup\$salvatop– salvatop2020年11月06日 19:54:57 +00:00Commented Nov 6, 2020 at 19:54