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
asobserveSingleEvent
'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
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.
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.