How to properly loop through a Firebase Query with completion handlers - ios

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

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 }
lock.lock()
defer { lock.unlock() }
for postData in values {
guard let restDict = (postData as AnyObject) as? PostData else { continue }
allPosts.append(restData)
}
}
})
}
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.

Related

Firebase function not getting called inside a forEach loop with a DispatchGroup

I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}

Chaining getDocument with Firestore?

user
29384092840923
chatRoomsJoined
chatRoom1
chatroom5
chatrooms
chatRoom1
users
29384092840923
298340982039490
I'm trying to load a tableview with information about the chat rooms a user has joined into. In the case above, user "29384092840923" has joined into chatRoom1, and I need the count of children of the users node in chatRoom1
My initial strat was to get an array of the joinedChatRooms from the "user" node and then do a for loop through and do a getDocument on each of the items in the array.
static func loadFavoriteRooms(forUID uid: String, completedFetch: #escaping (_ favoritedRoomsArray : [String]?, _ error : Error?)->()) {
let userFavoritesRef = database.collection("users").document(uid).collection("favoritedRooms")
userFavoritesRef.getDocuments { (snapshot, error) in
if error != nil {
completedFetch(nil, error!)
print("There was an error", error!.localizedDescription)
} else {
var roomArray = [String]()
for document in snapshot!.documents {
//Create a roomRef with the documentID, do a getDocument with it, and create an object with it?
let roomName = document.documentID
roomArray.append(roomName)
}
completedFetch(roomArray, nil)
}
}
}
My problem with what happened above was once I started sending off additional getDocument requests within the for-loop for the individual roomRefs, my completedFetch completion call was returning before the for loop was done asynchronously, and I wasn't getting a filled array back.
What's the cleanest way to do this? Do I need to do a dispatch group here or is there a better way to accomplish this? Using dispatch groups with firestore seems wrong here to me for some reason.
One possible option could be to use a DispatchGroup. Something like -
var roomArray = [String]()
let dispatchGroup = DispatchGroup()
for document in snapshot!.documents {
let roomId = document.documentID
let roomRef = database.collection("rooms").document(roomId)
dispatchGroup.enter()
roomRef.getDocument { (roomSnapshot, error) in
// Create the room from the snapshot here
roomArray.append(roomName)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main, execute: {
completedFetch(roomArray, nil)
})
Just make sure you got your .enter() and .leave() calls correct otherwise you'll get some very strange crashes.

Returning data from function in Firebase observer code block swift

I'm new to firebase and I want to know if is any possible way to return data in observer block. I have class ApiManager:NSObject and in this class I want to create all my firebase function that will return some kind of data from database. This is one of my function in this class
func downloadDailyQuote() -> [String:String] {
let reference = Database.database().reference().child("daily")
reference.observeSingleEvent(of: .value) { (snap) in
return snap.value as! [String:String] //I want to return this
}
return ["":""] //I don't want to return this
}
And if I now do something like let value = ApiManager().downloadDailyQuote(), value contains empty dictionary. Is any solution for that?
Update: When you call .observeSingleEvent, you call the method asynchronously. This means that the method will start working, but the response will come later and will not block the main thread. You invoke this method, but there is no data yet and therefore you return an empty dictionary.
If you use the completion block, then you will get the data as soon as the method action is completed.
func downloadDailyQuote(completion: #escaping ([String:String]) -> Void) {
let reference = Database.database().reference().child("daily")
reference.observeSingleEvent(of: .value) { (snap) in
if let dictionaryWithData = snap.value as? [String:String] {
completion(dictionaryWithData)
} else {
completion(["" : ""])
}
}
}

How to write a callback for the asynchronous function below

I have a function I'm using below. I want the data from that asynchronous function but it comes up nil if I try to use the data outside the fullResolutionImageData function. So I have to write a callback. But I don't know how to write the callback. Can someone show me how to write a callback for the asynchronous function below so I can access the data elsewhere. Can you show me how to write a callback so I can access files outside fullResolutionImageData asynchronous function.
public func fullResolutionImageData(completion: #escaping (Data?) -> ()) {
SharedQueues.imageProcessingQueue.addOperation { [path] in
let data = try? Data(contentsOf: URL(fileURLWithPath: path))
DispatchQueue.main.async {
completion(data)
}
}
}
In my view controller I have an action that uses this function
class ViewController: UIViewController{
func buttonAction(sender:UIButton!){
var photos2: [ImageSource]?
var files : [Data] = []
let _ = self.photos2?.map ({completion: path in
path.fullResolutionImageData{ data in
files.append(data) --> (prints 2)
}
})
print("(files.count)") --> (prints 0 How do I write callback?)
}
}
All u need is Dispatch Group.
func buttonAction(sender:UIButton!){
var photos2: [ImageSource]?
var files : [Data] = []
let myGroup = DispatchGroup()
let _ = self.photos2?.map ({completion: path in
myGroup.enter()
path.fullResolutionImageData{ data in
files.append(data) --> (prints 2)
myGroup.leave()
}
})
myGroup.notify(queue: DispatchQueue.main) { // 2
print("(files.count)")
}
}
Little bit of Explanation :
Dispatch groups are used for monitoring the completion of multiple asynchronous tasks. Like in your case getting the data from the file path which u are performing on background thread.
Things to be aware of :
Every single tasks that enters the group should leave the group no matter whether it succeeds in its goal or not. If any of the task entered the group fail to exit the thread will be blocked forever. So make sure fullResolutionImageData always executes the completion block!
EDIT:
Finally As pointed out by David in comments it makes sense to use foreach loop then using map if u are discarding the results
hence use
func buttonAction(sender:UIButton!){
var photos2: [ImageSource]?
var files : [Data] = []
let myGroup = DispatchGroup()
photos2?.forEach{ path in
myGroup.enter()
path.fullResolutionImageData{ data in
files.append(data) --> (prints 2)
myGroup.leave()
}
}
myGroup.notify(queue: DispatchQueue.main) { // 2
}
}

Firebase query using a list of ids (iOS)

I have an NSArray containing multiple ids. Is there a way in Firebase where I can get all the object with the ids in the array?
I am building a restaurant rating app which uses GeoFire to retrieve nearby restaurants. My problem is that GeoFire only returns a list of ids of restaurant that are nearby. Is there any way i can query for all the object with the ids?
No, you can't do a batch query like that in Firebase.
You will need to loop over your restaurant IDs and query each one using observeSingleEvent. For instance:
let restaurantIDs: NSArray = ...
let db = FIRDatabase.database().reference()
for id in restaurantIDs as! [String] {
db.child("Restaurants").child(id).observeSingleEvent(of: .value) {
(snapshot) in
let restaurant = snapshot.value as! [String: Any]
// Process restaurant...
}
}
If you are worried about performance, Firebase might be able to group all these observeSingleEvent calls and send them as a batch to the server, which may answer your original question after all ;-)
I know that this answer is considered accepted but I have had really good success using promise kit with the method frank posted with his javascript link Speed up fetching posts for my social network app by using query instead of observing a single event repeatedly and just wanted to share the swift version
So I have a list of users ids that are attached to a post like this:
also these methods are in my post class where I have access to the post id from firebase
// this gets the list of ids for the users to fetch ["userid1", "userid2"....]
func getParticipantsIds() -> Promise<[String]> {
return Promise { response in
let participants = ref?.child(self.key!).child("people")
participants?.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snapshotIds = snapshot.value as? [String] else {
response.reject(FirebaseError.noData)
return
}
response.fulfill(snapshotIds)
})
}
}
// this is the individual query to fetch the userid
private func getUserById(id:String) -> Promise<UserData> {
return Promise { response in
let userById = dbRef?.child("users").child(id)
userById?.observeSingleEvent(of: .value, with: { (snapshot) in
guard let value = snapshot.value else {
response.reject(FirebaseError.noData)
return
}
do {
let userData = try FirebaseDecoder().decode(UserData.self, from: value)
response.fulfill(userData)
} catch let error {
response.reject(error)
}
})
}
}
// this is the where the magic happens
func getPostUsers(compeltion: #escaping (_ users:[UserData], _ error:Error?) -> ()){
getParticipantsIds().thenMap { (id) in
return self.getUserById(id: id)
}.done { (users) in
compeltion(users, nil)
}.catch({ error in
compeltion([], error)
})
}

Resources