Firebase query using a list of ids (iOS) - 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)
})
}

Related

How to properly loop through a Firebase Query with completion handlers

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.

How to store Firebase data to access easily and fast in Swift

I am working on my user profile page on my app and I need to retrieve data from Firebase and display the data on my user profile page. I have managed to retrieve the data successfully from the database but the problem is that every time I go to the user profile page, it takes a little time to access the information in the database, so as soon as you go to the user profile page, the page will be empty. How can I avoid this?
Put another way, is there a way to access the data and store before going to the user profile page and then displaying the data that is stored? Here is my code:
// Setup the name label
func setupNameLabel() {
// Access the database and get the current user's name
Database.database().reference().child("Users").child(userID!).child("Name").observeSingleEvent(of: .value) { (snapshot) in
guard let name = snapshot.value as? String else { return }
self.nameLabel.text = name
}
view.addSubview(nameLabel)
}
// Setup the username label
func setupUsernameLabel() {
// Access the database and get the current user's username
Database.database().reference().child("Users").child(userID!).child("Username").observeSingleEvent(of: .value) { (snapshot) in
guard let username = snapshot.value as? String else { return }
self.usernameLabel.text = username
}
view.addSubview(usernameLabel)
}
// Setup the email label
func setupEmailLabel() {
// Access the database and get the current user's email
Database.database().reference().child("Users").child(userID!).child("Email").observeSingleEvent(of: .value) { (snapshot) in
guard let email = snapshot.value as? String else { return }
self.emailLabel.text = email
}
view.addSubview(emailLabel)
}
Try To make a Model and then using Single Method and Get Data with Completion
func getData(forUserID: String, completion: #escaping (Model) -> Swift.Void, error: #escaping (Bool) -> Void) {
}
Check models of Salada cocoapod, please.
https://github.com/1amageek/Salada
Especially the models like Relation, Disposer, Set, Array, File are very beautiful.

Firebase Observer Event Type

I’m trying to modify code written a by a previous programmer. He wrote a getPostFromFirebase() function where it updates the tableview when 1) the app loads up due to it’s presence in viewDidLoad and 2) when there is a new post add by a user. The problem is that he used a .observe(.childAdded) event type which means when a post is deleted or modified, the tableView will not update(my end goal to do). When I change .childAdded to .value, the current data doesn’t get loaded upon launch. I’ve been banging my head against the wall to figure out a if let statement to add a .value event type so the view can refresh after any change(if that’s even possible). I’m familiar with Firebase RT DB hence how I was able to ID the observer issue but I’m no where near close to as good as I’d like to be so any help is appreciated.
func getPostFromFirebase() {
let mostRecent = dbRef.lastestPostsQuery(count: 10)
mostRecent.keepSynced(true)
mostRecent.observe(.childAdded) { (snapshot: DataSnapshot) in
/*parse method in the PostFetcher class
that returns the post data or an error by way of a tuple.*/
let (post, error) = PostFetcher.parsePostSnapshot(snapshot: snapshot)
if let post = post {
self.latestPosts.append(post)
if let postId = post.postId { print("PostId = \(postId)") }
}
if let error = error {
print("\(#function) - \(error)")
}
}
}
Edit:
Thanks to Franks help I was able to implement his suggestion and added a .removeAll() to remove the current state and have the view append a fresh snapshot. Whether a post is added or deleted, the view now updates as I'd like it to do.
func getPostFromFirebase() {
let mostRecent = dbRef.lastestPostsQuery(count: 10)
mostRecent.keepSynced(true)
mostRecent.observe(.value) { (snapshot: DataSnapshot) in
self.latestPosts.removeAll()
for child in snapshot.children.allObjects as! [DataSnapshot] {
let (post, error) = PostFetcher.parsePostSnapshot(snapshot: child)
if let post = post {
self.latestPosts.append(post)
self.tableView.reloadData()
if let postId = post.postId { print("PostId = \(postId)") }
}
if let error = error {
print("\(#function) - \(error)")
}
}
}
}
The .child* events fire on child nodes of the location/query that you observe, while .value fires on the location/query itself. This means that the value you get is one level higher up in the JSON, and you'll need to loop over the results:
mostRecent.observe(.value) { (snapshot: DataSnapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
let (post, error) = PostFetcher.parsePostSnapshot(snapshot: child)
if let post = post {
self.latestPosts.append(post)
if let postId = post.postId { print("PostId = \(postId)") }
}
if let error = error {
print("\(#function) - \(error)")
}
}
}
Alternatively, you can listen to the .childChanged and .childRemoved events (in addition to .childAdded that you already have) and handle them separately. This may be better in cases where you need to update the UI efficiently, since it allows you to handle each individual case (new node, changed, node, removed node) in the most efficient way.

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.

How to use PromiseKit with Firebase in Swift?

So I import PromiseKit and then try
FIRDatabase.database().reference().child("somechild").removeValue().then {
/////
}
Obviously, this doesn't work and I was wondering what am I missing to make promises work with Firebase, if its even possible. What I'm trying to accomplish is to remove four Firebase references all at once with a single catch method.
With nodeJs I would easily use:
Promise.all ([
someRef.remove(),
someRef.remove(),
someRef.remove(),
someRef.remove()
]).then (function({
}).catch({
//handle error
})
Is there a way to accomplish this in Swift at all?
you can wrap Firebase function with fulfill and reject
/// Get chat IDs of user conversations
///
/// - Returns: array of user chat IDs
private func getUserChatIds() -> Promise<[String]> {
return Promise { fulfill, reject in
let userChatIDsRef = Database.database().reference()
.child(FireDatabasePaths.UserInfoPath.rawValue)
.child(userID).child("chatIDs")
userChatIDsRef.observe(.childAdded, with: { snapshot in
if let chatIDdic = snapshot.value as? [String: AnyObject] {
let keys = Array(chatIDdic.keys)
fulfill(keys)
} else {
reject(FirebaseError.empty)
}
})
}
}

Resources