Firebase Child Added Is Called Every time a Child is Changed - ios

I have a group messaging application that works fine until I want to change some of the basic group properties such as group title, image, etc. Before I show my code to display my conversations and update them I will show you some of my data structure.
When it comes to dealing with the displaying and editing of conversations I use two main nodes. An overall conversation node containing the conversation properties and a conversations node within my current user.
Here is what the conversation node in my current user looks like:
As you can see in the image above my user has a conversation node with a list of conversation ids. These conversation ids refer to a conversation node within my database. Here is a picture of the conversation node:
Just to review the problem. Basically when I update any of the conversation properties (title, image, members) it re calls my child added method which creates an error I will show later.
Here is my code to display the conversations:
func observeUserConversations() {
guard let uid = currentUserProperties.id else {
return
}
FIRDatabase.database().reference().child("users").child(uid).child("conversations").observe(.childAdded, with: { (snapshot) in
FIRDatabase.database().reference().child("conversations").child(snapshot.key).observe(.value, with: { (conversationSnapshot) in
if let conversation = Groups(snapshot: conversationSnapshot) {
conversation.groupId = conversationSnapshot.key
self.conversations.append(conversation)
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
}, withCancel: nil)
}, withCancel: nil)
}
Here is my code to update some of the conversation properties:
static func updateConversationProperties(conversationId: String, property: String, propertyValue: String) {
let updateConversationPropertyRef = FIRDatabase.database().reference().child("conversations").child(conversationId).child(property)
updateConversationPropertyRef.setValue(propertyValue)
ProgressHUD.showSuccess("Field Updated!")
}
Please note I have tried using update child values instead of set value and it still has the same bug.
To sum up whenever I update a conversation property the child added function is called and appends a duplicate version of the conversation to my conversation array.
I know this may be a bit confusing, so I have a video here showing the bug:
https://youtu.be/OhhnYzQRKi8
In the video above you will see that the same conversaiton is duplicated and added twice.
Any help would be appreciated!
UPDATE
So I changed my observers a bit to look like this:
FIRDatabase.database().reference().child("users").child(uid).child("conversations").observe(.childAdded, with: { (snapshot) in
FIRDatabase.database().reference().child("conversations").child(snapshot.key).observeSingleEvent(of: .value, with: { (conversationSnapshot) in
if let conversation = Groups(snapshot: conversationSnapshot) {
conversation.groupId = conversationSnapshot.key
self.conversations.append(conversation)
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
}, withCancel: nil)
}, withCancel: nil)
In the above code, everything works and no duplicates are made. However, now the conversations won't update in realtime. Instead they will display the old data and won't update to the newly changed data. Also if I add a conversation the new added conversaiton won't display.

Here is what I notice:
The way you had the code originally, the second listener was triggered any time a change is made to the value of /"conversations"/snapshot.key. And whenever this call was made, you were appending the conversationSnapshot to conversations array:
FIRDatabase.database().reference().child("users").child(uid).child("conversations").observe(.childAdded, with: { (snapshot) in
FIRDatabase.database().reference().child("conversations").child(snapshot.key).observe(of: .value, with: { (conversationSnapshot) in
if let conversation = Groups(snapshot: conversationSnapshot) {
conversation.groupId = conversationSnapshot.key
self.conversations.append(conversation) // here is where you are appending the data. This will be appended each time a change is made
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
}, withCancel: nil)
}, withCancel: nil)
Now as you point out, if you change FIRDatabase.database().reference().child("conversations").child(snapshot.key).observe to .observeSingleEvent, the data won't append again, but you won't get updates. One option is whenever the listener is triggered, you search the array for the snapshot key, and then update the snapshot at that index if found. Not the most efficient method, to be sure.
In summation, it sounds like you do need to use observe, but as it stands, the data is duplicated because the code appends the snapshot to the end of the array whenever a change is made to the snapshot's value. You will have to use something other than self.conversations.append(conversation).
I'd be happy to brainstorm some other options if you wanted to message me directly.

FIRDatabase.database().reference().child("users").child(uid).child("conversations").observe(.childAdded, with: { (snapshot) in
FIRDatabase.database().reference().child("conversations").observe(.childAdded, with: { (conversationAdded) in
if conversationAdded.key == snapshot.key {
if let group = Groups(snapshot: conversationAdded) {
self.conversations.append(group)
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
}
})
}, withCancel: nil)
FIRDatabase.database().reference().child("users").child(uid).child("conversations").observe(.childAdded, with: { (snapshot) in
FIRDatabase.database().reference().child("conversations").child(snapshot.key).observe(.childChanged, with: { (conversationSnapshot) in
let conversationIdsArray = self.conversations.map({$0.groupId})
let changeAtGroupIdIndex = conversationIdsArray.index(of: snapshot.key)
let conversationToBeUpdated = self.conversations[changeAtGroupIdIndex!]
conversationToBeUpdated.setValue(conversationSnapshot.value, forKeyPath: conversationSnapshot.key)
self.tableView.reloadData()
}, withCancel: nil)
}, withCancel: nil)
In the above code, I create two different observers. The first one loads conversations when the app is loaded or a conversation is added. The second one updates the conversation array if the child has been changed. This solves both problems.

Filter array with data excluding the object just received. Identify that object in the existing array by a unique id like groupID or chatID in my case. Then the repeated object will be removed
self.conversations = self.conversations.filter { obj in (obj.chatId as? String) != (data.chatId as? String) }
self.conversations.append(data)

Related

Trying to figure out a better way to delete element from Firestore dictionary

I'm using Firestore in my application, and I have a map field called "votes" for user's upvotes or downvotes. It looks like this:
I want to add an option to delete an element from there, this is what I got now:
//getting the user's votes dictionary and removing the post from it.
userRef.getDocument { (doc, error) in
if let _ = error { completion(false) }
guard let dict = doc?.data()?[USER_VOTES] as? [String: Any] else { return }
currentDict = dict
currentDict.removeValue(forKey: id)
}
//setting the votes dictionary with the updated one.
userRef.setData(currentDict) { (error) in
if let _ = error { completion(false) }
else { completion(true) }
}
to me, It looks not really efficient, because each time a user is trying to remove an element from this dictionary, I have to write to the database. which can slow down the process and to my understanding, the free tier of Firestore limits the number of writes.
Is there a better way, maybe deleting it right from the user's document? I tried to look for answers, but couldn't find anything that worked for me.
This one for example: Removing a dictionary element in Firebase looks like what I need to do, but I couldn't get it to work.
EDIT:
I tried deleting it like that
userRef.updateData([
USER_VOTES:[
id: FieldValue.delete()
]
]) { (error) in
if let _ = error { completion(false) }
}
The app crashes says:
Terminating app due to uncaught exception 'FIRInvalidArgumentException', reason: 'FieldValue.delete() can only appear at the top level of your update data
To be able to delete a specific field you should follow the steps mentioned here.
For your case I have created the following under collection 'voting':
So to delete vote2 field you should use:
// Get the `FieldValue` object
let FieldValue = require('firebase-admin').firestore.FieldValue;
// Create a document reference
let fieldRef = db.collection('voting').doc('votes');
// Remove the 'vote2' field from the document 'votes'
let removeField = fieldRef.update({
vote2: FieldValue.delete()
});
And here is the document after running the above:
EDIT :
If the data model is a map inside a document, for example:
Then here is how you can delete a field inside the array which is inside the document:
let docRef = db.collection('voting').doc('user1');
let removeField = docRef.set({'votes':
{['id_vote_1'] : FieldValue.delete()}}, {merge: true});
Here is the document after running the above:

How does control flow work when retrieving Information from Firebase?

var ergebnisBluetezeit = Set<String>()
let refBluetezeit = rootRef.child("Pflanzen").child("Eigenschaften").child("Blütezeit")
refBluetezeit.child("Februar").observeSingleEvent(of: .value, with: { snapshot in
for plant in snapshot.children {
self.ergebnisBluetezeit.insert((plant as AnyObject).value)
}
})
print(ergebnisBluetezeit)
I want to retrieve Data from my Firebase Database. The Retrieving Process does work already, but the following confuses me: the current output from the print is an empty set, but when i use the var ergebnisBluetezeit elsewhere (for example setup a button, which action is to print ergebnisBluetezeit), it is filled. When i put the print in the for loop, it does print the right output, too.
I seem to not have understood the control flow here, so my Question:
How can i use the Set where the print statement is at the moment?
Thanks for your help.
It's the logic of asynchronous calls
print("1") // empty
refBluetezeit.child("Februar").observeSingleEvent(of: .value, with: { snapshot in
print("3") // empty
for plant in snapshot.children {
self.ergebnisBluetezeit.insert((plant as AnyObject).value)
}
print(ergebnisBluetezeit) // not empty
})
print("2") // empty
the value is empty until the request finishes regardless of where in code ordering you run the print , as the numbering above in order 1 , 2 , 3 to know when it finishes you can use completions like
func getData(completion:#escaping() -> ()) {
let refBluetezeit = rootRef.child("Pflanzen").child("Eigenschaften").child("Blütezeit")
refBluetezeit.child("Februar").observeSingleEvent(of: .value, with: { snapshot in
for plant in snapshot.children {
self.ergebnisBluetezeit.insert((plant as AnyObject).value)
}
completion()
})
}
And call
getData {
print(ergebnisBluetezeit)
}

Listing Firebase data and relationships

I have just started working with Firebase database and I am a bit confused how to structure the database. In the following example I have a users object and a groups object. Each user can be part of multiple groups and every group can have multiple users. The proposed database structure is the following, according to "Structure Your Database".
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"groups": {
"techpioneers": true,
"womentechmakers": true
}
}
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"startDate": "24-04-1820",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
}
}
}
Let's say I want to display all groups in a list in my app, with the group name and start date. How would I make that database call? Since the user object only contains the id of the groups, would I then have to make a separate call to the database for every group just to find out the name and start date of the group? If there are many groups in the list, then that becomes a lot of calls. My group might contain a lot of other information as well so this doesn't seem good for performance. Can I get all the groups in the groups list of the user, in one call?
One though I had was to include the name and start date in the groups object under the user:
"users": {
"alovelace": {
"name": "Ada Lovelace",
"groups": {
"techpioneers":{
"name": "Historical Tech Pioneers",
"startDate": "24-04-1820"
},
"womentechmakers":{
"name": "Women in Technology",
"startDate": "13-10-1823"
}
}
}
}
}
but this solution seems to add a lot of duplicate data. Also if I want to update the name I would have to do that in multiple locations. And maybe I want to add a sponsor organization object, that also contains group, and then want to list them. Then there would be 3 places to update the information on. How would I solve this?
You would then have two possibilities, one would be to store the data you need (duplicating it) in the groups node of each user.
The other, which is the one that I would recommend the most, would be to add an observeSingleEvent(of: .value) inside your first observer (that could be an observe(.value), observe(.childAdded) or whatever).
Say you have an array of all your group members, and an object named AppUser that represents a user in your app :
var groupMembers = [AppUser]()
To detect whenever a new member is added to a group for example, you could use a .childAdded observer for example :
func groupWasAddedObserver(completion: #escaping () -> ()) {
// Add a .childAdded observer to the group's members node (groupId should be defined somewhere) :
groupsRef.child(groupId).child("members").observe(.childAdded, with: { [weak self] (snapshot) in
// When you get the snapshot, store its key, which represents the member id :
let memberId = snapshot.key
// fetch this member's profile information using its id :
self?.getUser(memberId, completion: { (groupMember) in
// Once you got the profile information of this member, add it to an array of all your members for example :
self?.groupMembers.append(groupMember)
// Call the completion handler so that you can update the UI or reload a table view somewhere maybe depending on your needs :
completion()
})
})
}
And the second method to fetch a user data knowing his or her id :
func getUser(_ userId: String, completion: #escaping (AppUser) -> ()) {
// Add the observerSingleEvent observer :
usersRef.child(userId).observeSingleEvent(of: .value, with: { (snapshot) in
// Get the data you need using the snapshot you get :
guard let email = snapshot.childSnapshot(forPath: "email").value as? String else { return }
guard let name = snapshot.childSnapshot(forPath: "name").value as? String else { return }
guard let picUrl = snapshot.childSnapshot(forPath: "picUrl").value as? String else { return }
// Call the completion handler to return your user/member :
completion(AppUser(id: snapshot.key, email: email, name: name, picUrl: picUrl))
})
}
As you can see you get the memberId of each user using the snapshot key, and you use this memberId to fetch this specific user data.

Output order becomes different in nested query, using Firebase 3, Swift

I'm using Firebase to store user info, and I have this nested function that fetch the post info, and then using the UID in post to fetch the user info.
Nested function to fetch post info and then fetch user
func fetchUser(completion: #escaping (User) -> Void) {
REF_POST.queryOrdered(byChild: "timestamp").observe(.childAdded, with: { (postData) in
let post = ConvertPost(data: postData.key)
print(post.uid) >>>>>>UID ordered by timestamp<<<<<<<<
REF_USER.child(post.uid).observeSingleEvent(of: .value, with: { (userData) in
print(post.uid) >>>>>>UID order becomes different<<<<<<<<
let user = ConvertUser(data: userData)
completion(user)
})
}
I have a print(uid) before observing the users, the output is ordered by timestamp, which is what I want:
PXT6********
WT7i********
WT7i********
PXT6********
And a print(uid) inside observing users, the output order is different:
WT7i********
WT7i********
PXT6********
PXT6********
so my question is why the order becomes different?
I'm calling the method in ViewDidLoad()
Is it something to do with the closure block?
Question Update
After some testing, I found that the output will always group the same uid together, something like A,A,B,B,C,C. Please help me.
Use this code below:
func observeUsers(uid: String, completion: #escaping (User) -> Void) {
print(uid)
REF_USERS.keepSynced(true) // <-- this will make sure your code will update with fresh data
REF_USERS.child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
print(uid)
let user = ConvertUser(data: snapshot.value)
completion(user)
}
})
}
Either use that code, or disable data persistance in your appDelegate. More information:Firebase : What is the difference between setPersistenceEnabled and keepSynced? and in the docs of Firebase ofcourse.

Showing post from currentUsers wall - Firebase / Swift

I am trying to create a wall/timeline that shows posts from all the users that currentUser is following. All users that currentUser is following is showed under Users -> UserID -> Following. Whenever one of their followers is making a post it will be added under feed-items with an autoID - the key (the autoID) is added to currentUsers Users -> UserID -> Wall at the same time.
Here is an image of an example from my Firebase database:
Under Wall as you can see, one of this users followers has made a post (the whole post is saved under feed-items) and the autoID of that post has made it to the users Wall.
Now I am trying to figure out how to show all the posts in feed-items, based on the autoID's stored under currentUsers Wall.
I have tried the following code, but nothing shows and when it reaches this line print(self.updates.count) it is printing 0.
func startObersvingDB(userID: String) {
FIRDatabase.database().reference().child("Users").child(userID).child("Wall").observeEventType(.ChildAdded, withBlock: { snapshot in
if let posts = snapshot.value!["Post"] as? String {
self.postArray.append(posts)
for i in 0..<self.postArray.count {
let post = self.postArray[i]
print(post)
FIRDatabase.database().reference().child("feed-items").queryEqualToValue(post).observeEventType(.Value, withBlock: { (snapshot: FIRDataSnapshot) in
var newUpdates = [Sweet]()
for update in snapshot.children {
let updateObject = Sweet(snapshot: update as! FIRDataSnapshot)
newUpdates.append(updateObject)
}
self.updates = newUpdates.reverse()
print(self.updates.count)
self.tableView.reloadData()
}) { (error: NSError) in
print(error.description)
}
}
}
})
}
If I am guessing right, your structure for feed-items is something like this.
feed-items
-UniquePostID
-Post Data (key-value pair(s))
If this is the case then to retrieve data for a post use .child(post) instead of .queryEqualToValue(post). Also since this will return DataSnapshot for single post you can directly create your Sweet object and append it in existing updates array.
One more thing I don't think you need to iterate entire postArray each time a new post is added. You should retrieve data for new post only.
Hope this helps!!

Resources