0

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?

DJ-Glock
  • 1,277
  • 2
  • 12
  • 39

1 Answers1

0

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)
        }
    })
}
DJ-Glock
  • 1,277
  • 2
  • 12
  • 39