Async call blocking main thread when using DispatchGroup - ios

I am trying to get documents from a FireStore database. I need these documents to be loaded before moving forward in my function. Here is the code for reference:
The view controller calling the FireStore service functions:
let service = FirestoreServices()
service.get(cottage: "test123456789") { model in
nextViewController.cottageModel = model!
self.present(nextViewController, animated:true, completion:nil)
}
The FireStore service method being called:
func get(cottage: String, completionHandler: #escaping (CottageTrip?) -> ()) {
//get a reference to the firestore
let db = Firestore.firestore()
//get the references to the collections
let cottageRef = db.collection("cottages").document(cottage)
//get the fields from the initial document and load them into the cottage model
cottageRef.getDocument { (document, error) in
if let document = document, document.exists {
//create the cottage model to return
let cottageModel: CottageTrip = CottageTrip()
//get the subcollections of this document
let attendeesCollection = cottageRef.collection("attendees")
//other collections
//here I get all info from initial document and store it in the model
let group = DispatchGroup()
print("here")
group.enter()
//get the attendees
DispatchQueue.global(qos: .userInitiated).async {
attendeesCollection.getDocuments() { (querySnapshot, err) in
print("here2")
if let err = err {
print("Error getting documents: \(err)")
} else {
//get data
}
group.leave()
}
}
print("after async call")
//wait here until the attendees list is built
group.wait()
print("after wait")
//create the cars
carsCollection.getDocuments() { (querySnapshot, err) in
print("in car collection get doc call")
if let err = err {
print("Error getting documents: \(err)")
} else {
//get car data
}
}
}
//this is where she should block until all previous get document operations complete
completionHandler(cottageModel)
} else {
print("Document does not exist")
completionHandler(nil)
}
}
}
I am realizing that the print("here2") never prints so it seems like it blocks on the group.wait(). I need to use group.wait() rather than a notify because I need this function to access subcollections and documents only after the attendees collection is loaded as I need these values for the subcollections and documents. I have read a lot of answers online and most people use group.wait() in this scenario but for some reason I can not get it to work for me without locking and freezing the application.

As algrid pointed out, you have a deadlock because you are waiting on the main thread, which Firestore needs to call its closures.
As a general rule, avoid calling wait and you will not deadlock. Use notify, and just call your closure inside that notify closure.
So, for example, assuming that you do not need the results from attendees in order to query the cars, you can just use a notify dispatch group pattern, e.g.
func get(cottage: String, completionHandler: #escaping (CottageTrip?) -> Void) {
let db = Firestore.firestore()
let cottageRef = db.collection("cottages").document(cottage)
cottageRef.getDocument { document, error in
guard let document = document, document.exists else {
print("Document does not exist")
completionHandler(nil)
return
}
let cottageModel = CottageTrip()
let attendeesCollection = cottageRef.collection("attendees")
let carsCollection = cottageRef.collection("cars")
let group = DispatchGroup()
group.enter()
attendeesCollection.getDocuments() { querySnapshot, err in
defer { group.leave() }
...
}
group.enter()
carsCollection.getDocuments() { querySnapshot, err in
defer { group.leave() }
...
}
group.notify(queue: .main) {
completionHandler(cottageModel)
}
}
}
Also, as an aside, but you do not have to dispatch anything to a global queue, as these methods are already asynchronous.
If you needed the result from one in order to initiate the next, you can just nest them. This will be slower (because you magnify the network latency effect), but also eliminates the need for the group at all:
func get(cottage: String, completionHandler: #escaping (CottageTrip?) -> Void) {
let db = Firestore.firestore()
let cottageRef = db.collection("cottages").document(cottage)
cottageRef.getDocument { document, error in
guard let document = document, document.exists else {
print("Document does not exist")
completionHandler(nil)
return
}
let cottageModel = CottageTrip()
let attendeesCollection = cottageRef.collection("attendees")
let carsCollection = cottageRef.collection("cars")
attendeesCollection.getDocuments() { querySnapshot, err in
...
carsCollection.getDocuments() { querySnapshot, err in
...
completionHandler(cottageModel)
}
}
}
}
Either way, I might be inclined to break this up into separate functions, as it is a little hairy, but the idea would be the same.

Related

Completion Handler not working Properly in swift

im trying to populate two arrays with the data i get from the firestore database. im getting the data successfully however it was late and when i printed them in viewDidLoad it printed empty arrays. so i decided to implement a completion handler however it still shows and empty array. can anyone tell me why my print statement runs before the functions even though im using escaping
func yourFunctionName(finished: #escaping () -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries.append(document.documentID)
}
}
}
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries2.append(document.documentID)
}
}
}
finished()
}
viewDidLoad(){
yourFunctionName {
print(self.countries)
print(self.countries2)
}
}
i get the empty arrays in the output although the arrays should have been filled before i called print though im using #escaping. please someone help me here
You are actually not escaping the closure.
For what I know the "#escaping" is a tag that the developper of a function use to signify the person using the function that the closure he/she is passing will be stored and call later (after the function ends) for asynchronicity and memory management. In your case you call the closure passed immediately in the function itself. Hence the closure is not escaping.
Also the firebase database is asynchronous. Meaning that you don't receive the result immediately
This part :
{ (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries.append(document.documentID)
}
}
}
is itself a closure, that will be executed later when the result of the query is produced. As you can see in the doc, the function is escaping the closure : https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query.html#getdocumentssource:completion:
func getDocuments(source: FirestoreSource, completion: #escaping FIRQuerySnapshotBlock)
So to summarise :
The code for the firebase query will be call later (but you don't know when), and your closure "finished" is called immediately after having define the firebase callback, thus before it has been called.
You should call your finished closure inside the firebase callback to have it when the arrays are populated.
I think your main problem here is not about to populate your arrays, your problem is how to get it better.
I did an example of how you could do that in a better way.
First, break your big function in two, and populate it out of your function.
Look at this code and observe the viewDidLoad implementation.
func countries(withCapital capital: String, completionHandler: (Result<Int, Error>) -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: capital)
.getDocuments { (snapshot, error) in
guard error == nil else {
completionHandler(.failure(error!))
return
}
let documents = snapshot!.documents
let ids = documents.map { $0.documentID }
completionHandler(.success(ids))
}
}
func countries(withClimate climate: String, completionHandler: (Result<Int, Error>) -> Void) {
db.collection("countries")
.whereField("climate", isEqualTo: climate)
.getDocuments { (snapshot, error) in
guard error == nil else {
completionHandler(.failure(error!))
return
}
let documents = snapshot!.documents
let ids = documents.map { $0.documentID }
completionHandler(.success(ids))
}
}
func viewDidLoad(){
countries(withClimate: "pleasant") { (result) in
switch result {
case .success(let countries):
print(countries)
self.countries2 = countries
default:
break
}
}
countries(withCapital: "washington") { (result) in
switch result {
case .success(let countries):
print(countries)
self.countries = countries
default:
break
}
}
}
If you have to call on main thread call using it
DispathQueue.main.async {
// code here
}
I hope it helped you.
They are returning empty arrays because Firebase's function is actually asynchronous (meaning it can run after the function "yourFunctionName" has done its work)
in order for it to work as intended (print the filled arrays)
All you need to do is call it inside Firebase's closure itself, like so:
func yourFunctionName(finished: #escaping () -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
self.countries.append(document.documentID)
finished() //<<< here
}
}
}
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
self.countries2.append(document.documentID)
finished() //<<< and here
}
}
}
}
I has been sometime since I encountered that problem but I beleve the issue is that you are calling the completion handler to late. What I mean is that you can try to call it directly after you have lopped throught the documents. One idea could be to return it in the compltion or just do as you do. Try this instead:
func yourFunctionName(finished: #escaping ([YourDataType]?) -> Void) {
var countires: [Your Data Type] = []
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
countries.append(document.documentID)
}
finished(countries)
return
}
}
}
func yourSecondName(finished: #escaping([YouDataType]?) -> Void) {
var countries: [Your data type] = []
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
//print(document.documentID)
//print(documentData)
countires.append(document.documentID)
}
finished(countires)
return
}
}
func load() {
yourFunctionName() { countries in
print(countires)
}
yourSecondName() { countries in
print(countries)
}
}
viewDidLoad(){
load()
}
What this will do is that when you call the completion block that is of type #escaping as well as returning after it you won't respond to that completely block any longer and therefore will just use the data received and therefore not care about that function anymore.
I good practice according to me, is to return the object in the completion block and use separate functions to be easier to debug and more efficient as well does it let you return using #escaping and after that return.
You can use a separate method as I showed to combine both methods and to update the UI. If you are going to update the UI remember to fetch the main queue using:
DispathQueue.main.async {
// Update the UI here
}
That should work. Greate question and hope it helps!

DispatchGroup with async functions in a Firebase Realtime DB observe method in Swift

I'm developing an iOS chat app that uses Firebase Realtime Database for storing messages. I have a function that is called when the home chat screen is loaded. This function loads the recipient name, last message, timestamp and a profile pic.
I've used DispatchGroup to sync all the calls. At first, I thought that it worked, but when I send a new message (update the DB in any way) the app crashes. I believe it is because the observe closure is being called again, and there is an imbalance between the enter/leave calls.
I can't think of a way to make it work with DispatchGroup. Is there a way to fix this? Or is there a better option than DispatchGroup?
This is the main function with the firebase observer:
func getAllChatsForCurrentUser(completion: #escaping (_ chats: [Chat], _ error: Error?) -> Void) {
var chats = [Chat]()
let group = DispatchGroup()
let currentUserUID = Auth.auth().currentUser!.uid
let chatRef = Database.database().reference(withPath: "chats")
group.enter()
chatRef.observe(.value) { (snapshot) in
var childrenArray = [String]()
let children = snapshot.children
while let rest = children.nextObject() as? DataSnapshot {
childrenArray.append(rest.key) //1
}
for child in childrenArray {
if child.contains(currentUserUID) { //2
let otherUserUID = child.replacingOccurrences(of: currentUserUID, with: "")
group.enter()
self.getChatInfo(uid: otherUserUID, chatID: child) { (chat, err) in
chats.append(chat)
group.leave()
}
}
}
group.leave()
}
group.notify(queue: .main) {
completion(chats, nil)
}
}
1 - For the chat name I use a combination of 2 uid's. So here I have an array of all chats.
2 - If the chat name contains the current users uid - I'm working with it. The recipients uid in the other part of the string.
getChatInfo function below:
func getChatInfo(uid: String, chatID: String, completion: #escaping (_ chat: Chat, _ error: Error?) -> Void) {
let miniGroup = DispatchGroup()
var newChat = Chat()
newChat.otherUserUid = uid
miniGroup.enter()
self.getUserProfileFromUID(uid: uid) { (user, error) in
newChat.name = user.name
newChat.profilePic = user.photoURL
miniGroup.leave()
}
miniGroup.enter()
self.getLastMessageAndTimeForChat(chatID: chatID) { (message, time, error) in
newChat.lastMessage = message
newChat.lastMessageTime = time
miniGroup.leave()
}
miniGroup.notify(queue: .main) {
completion(newChat, nil)
}
}
I know that this is probably a bad way of structuring the data and calling the functions. At least I've been told so, without reasoning. Been stuck with this problem for nearly a week now, any info would be greatly appreciated.
UPDATE 1
Tried wrapping the leave() calls in defer {}, and tried playing around with NSOperations instead of DispatchGroup. Still no luck.
So I figured it out by using a completion handler with a begin handler.
getChatsWithBeginAndComplete(beginHandler: {
self.group.enter()
self.group.notify(queue: .main) {
print("done")
self.tableView.reloadData()
}
}) {
self.group.leave()
}
And the function:
func getChatsWithBeginAndComplete(beginHandler: #escaping () -> (), completionHandler: #escaping () -> ()) {
allChatsHandle = allChatsRef.observe(.value) { (snapshot) in
let bigGroup = DispatchGroup()
beginHandler()
var childrenArray = [String]()
let children = snapshot.children
while let rest = children.nextObject() as? DataSnapshot {
childrenArray.append(rest.key)
}
for chatID in childrenArray {
if chatID.contains(currentUserUID) {
bigGroup.enter()
let funcGroup = DispatchGroup()
//Do more async stuff in the funcGroup
funcGroup.notify(queue: .main) {
self.chats.append(chat)
bigGroup.leave()
}
}
}
bigGroup.notify(queue: .main) {
completionHandler()
}
}
}
So here all the group.enter and group.leave calls are balanced, because they are called from the completion/begin handlers, or from inside the firebase observer.
I don't think that it's the best way to handle this problem, but it's definitely one way. If somebody knows a better solution - please let me know.

Closure returning data before async work is done

UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("🌈🌈🌈🌈🌈🌈🌈In closure function to update articles🌈🌈🌈🌈🌈🌈🌈")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("🌈🌈🌈🌈🌈🌈🌈 sending back completion array \(returnArray)🌈🌈🌈🌈🌈🌈🌈")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}

Firestore: Calls with of async function with SnapshotListener and in a cycle with DispatchGroup cause a crash

I have an issue with using DispatchGroup (as it was recommended here) with FireStore snapshotListener
In my example I have two functions. The first one is being called by the ViewController and should return array of objects to be displayed in the View.
The second one is a function to get child object from FireStore for each array member. Both of them must be executed asynchronously. The second one should be called in cycle.
So I used DispatchGroup to wait till all executions of the second function are completed to call the UI update. Here is my code (see commented section):
/// Async function returns all tables with active sessions (if any)
class func getTablesWithActiveSessionsAsync(completion: #escaping ([Table], Error?) -> Void) {
let tablesCollection = userData
.collection("Tables")
.order(by: "name", descending: false)
tablesCollection.addSnapshotListener { (snapshot, error) in
var tables = [Table]()
if let error = error {
completion (tables, error)
}
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let firebaseID = document.documentID
let tableName = data["name"] as! String
let tableCapacity = data["capacity"] as! Int16
let table = Table(firebaseID: firebaseID, tableName: tableName, tableCapacity: tableCapacity)
tables.append(table)
}
}
// Get active sessions for each table.
// Run completion only when the last one is processed.
let dispatchGroup = DispatchGroup()
for table in tables {
dispatchGroup.enter()
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(tables, nil)
}
}
}
/// Async function returns table session for table or nil if no active session is opened.
class func getActiveTableSessionAsync (forTable table: Table, completion: #escaping (TableSession?, Error?) -> Void) {
let tableSessionCollection = userData
.collection("Tables")
.document(table.firebaseID!)
.collection("ActiveSessions")
tableSessionCollection.addSnapshotListener { (snapshot, error) in
if let error = error {
completion(nil, error)
return
}
if let snapshot = snapshot {
guard snapshot.documents.count != 0 else { completion(nil, error); return }
// some other code
}
completion(nil,nil)
}
}
Everything works fine till the moment when the snapshot is changed because of using a snapshotListener in the second function. When data is changed, the following closure is getting called:
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
And it fails on the dispatchGroup.leave() step, because at the moment group is empty.
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
All dispatchGroup.enter() and dispatchGroup.leave() are already done on this step. And this closure was called by listener separately.
I tried to find the way how to check if the DispatchGroup is empty to do not call leave() method. But did not find any native solution.
The only similar solution I've found is in the following answer. But it looks too hacky and not sure if will work properly.
Is there any way to check if DispatchGroup is empty? According to this answer, there is no way to do it. But probably something changed during last 2 years.
Is there any other way to fix this issue and keep snapshotListener in place?
For now I implemented some kind of workaround solution - to use a counter.
I do not feel it's the best solution, but at least work for now.
// Get active sessions for each table.
// Run completion only when the last one is processed.
var counter = tables.count
for table in tables {
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
counter = counter - 1
if (counter <= 0) {
completion(tables, nil)
}
})
}

Completion block for nested requests

I am trying to build methods with completion blocks for nested requests. The issue is that a completion block catches to early for parent requests (meaning that the child requests haven't actually completed yet). So far I haven't found a way for a child request to communicate back to the parent request other than what I've done in my example below (which is to count the amount of child requests have completed and compare it against the expected amount of requests).
The example below is working against a Firestore database. Imagine a user has multiple card games (decks) with each multiple cards. I'm grateful for any help how to build better completion blocks for cases like these:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var decksCount = Int()
var cardsCount = Int()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
decksCount += 1
cardsCount += cardSnapshot.count
if decksCount == deckSnapshot.count {
completion(cardsCount)
}
}
})
})
}
}
}
Here is the solution, using DispatchGroup, found with #meggar's help in the comments:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var cardsCount = Int()
let group = DispatchGroup()
group.enter()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
group.enter()
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
cardsCount += cardSnapshot.count
}
group.leave()
})
})
}
group.leave()
}
group.notify(queue: .main) {
completion(cardsCount)
}
}

Resources