Home > Mobile >  How to properly loop through a Firebase Query with completion handlers
How to properly loop through a Firebase Query with completion handlers

Time:12-22

Problem:

I need to loop through a Firebase query call.

My loop is initiated before the Firebase call as it holds the uids needed.

for uid in following_uids {
}

What is the proper way to loop variable uid into the Firebase reference & query?

for uid in following_uids {
    let fbRef = ref.child("users").child(uid).child("posts")
    //query firebase call
}

Replication:

func getRecentPostsFromFollowers(following_uids: [String]) {
    for uid in following_uids {// first loop
        let fbRef = ref.child("users").child(uid).child("posts")
            
        fbRef.queryOrderedByKey().queryLimited(toLast: 5).observeSingleEvent(of: .value, with: {snapshot in
            if(snapshot.exists()){
                let values = snapshot.children.compactMap { ($0 as? DataSnapshot)?.value }
                  
                for postData in values {
                    guard let restDict = (postData  as AnyObject) as? [String: Any] else { continue }
                    //do with data
                }
            }
        })
    }//end first loop
        
    print("Completion handler - Loop Done")
}

PSEUDO code:

func getFollowerUIDS(completion: @escaping (_ followers: [String]) -> Void)  {
    for uid in following_uids {
        let fbRef = ref.child("users").child(uid).child("posts")
        //query firebase call
    }
    completion(value)
}

Specs:

Xcode Version 14.2 (14C18) iOS

CodePudding user response:

I'm assuming you'd like to combine the results from all user posts and return them as a single array of posts.

When you call observeSingleEvent on your document reference, the second parameter is a type of closure. This is essentially just another function that the Firebase SDK will call when it has the data ready for you.

The idea is that: because it may take some time to fetch this data, we don't want to block the rest of your function/code from running while the network call is taking place. This means you will likely see the end of the loop "Completion handler - Loop Done" called before your data is made available. This means your getRecentPostsFromFollowers function will return before any of your closures are called with data.

In order to allow callers of your getRecentPostsFromFollowers function to know when the data is ready, you can add your own completion handler to provide this data back to them. This is a closure that you will call when you know all the data is ready.

However, because there's multiple closures with data that we need to combine, we can use something like a DispatchGroup for this purpose. We'll combine the posts in an allPosts variable and return the data when it's combined from all the requests.

We need to lock around the access to allPosts as observeSingleEvent's completion handler can run on any thread, and multiple threads could try to access this variable at once.

typealias PostData = [String: Any]

func getRecentPostsFromFollowers(following_uids: [String], completion: @escaping ([PostData]) -> Void) {
    let group = DispatchGroup()
    let lock = NSLock()
    var allPosts = [PostData]()
    for uid in following_uids {
        let fbRef = ref.child("users").child(uid).child("posts")
            
        group.enter()
        fbRef.queryOrderedByKey().queryLimited(toLast: 5).observeSingleEvent(of: .value, with: {snapshot in
            defer { group.leave() }
            if(snapshot.exists()){
                let values = snapshot.children.compactMap { ($0 as? DataSnapshot)?.value }
                  
                for postData in values {
                    guard let restDict = (postData as AnyObject) as? PostData else { continue }
                    lock.lock()
                    allPosts.append(restData)
                    lock.unlock()
                }
            }
        })
    }
    
    group.notify(queue: .main) {
        completion(allPosts)
    }
}

Side Notes

  1. Swift async/await is a more modern way to handle asynchronous-based tasks like this without some of the pitfalls in this solution, it's worth looking into.

  2. Performing a separate Firestore query for each user to retrieve all their data is not very efficient. This is because each query requires a separate network request and will cost you at least one document read per query made, regardless if there are results or not per-user. You should consider structuring your data within your Firestore database so you can return all the data you require in a single query. This may involve denormalizing some of your data to allow for more efficient queries.

  • Related