Can someone clearly explain difference between .Value, .ChildAdded, .ChildChanged, .ChildRemoved for FIRDataEventType? - ios

I'm having trouble putting it into words. Can someone explain what the difference between the different FIRDataEventTypes and examples of when it would be used?
Example (SWIFT):
let queryRef = FIRDatabase.database().reference().child("user")
queryRef.observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
or
queryRef.observeEventType(.Value, withBlock: { (snapshot) -> Void in
From testing, .Value returns one object while .ChildAdded returns multiple; When doing advanced queries .ChildAdded doesn't work but .Value somewhat works (deeper children are null).

tl;dr - Watch this video. It uses the old SDK in Android, but the idea is the exact same even for iOS.
Each one of these events is a specific way to handle synchronization of data across clients.
The Value event will fire each time any piece of data is updated. This could be a newly added key, a deletion of a key, or an update of any value at the reference. When the change happens the SDK sends back the entire state of the object, not the delta just change that occurred.
The Child added event will fire off once per existing piece of data, the snapshot value will be an individual record rather than the entire list like you would get with the value event. As more items come in, this event will fire off with each item.
The Child removed and changed events work almost the same. When an item is deleted or has it's value changed, the individual item is returned in the callback.

Related

Swift -Firebase how to know if .childAdded is empty?

I know when using .value I can check to see if there is anything there using .exists()
In this situation I’m running my queries in a SearchController and I need multiple results to get returned. Everything works fine when there are results but if there aren’t any I have no way of knowing.
How can I check if .childAdded returns nothing?
// if the user types in McDonalds and there are some then multiple will appear but if none then show a label that says “no results”
let searchText = searchController.searcchBar.text?.loweredCase() else { return }
Database.reference.child("restaurants")
.queryOrdered(byChild:"restaurantName")
.queryStarting(atValue: searchText)
.queryEnding(atValue: searchText + “\u{f8ff}”)
.observe( .childAdded, with: { (snapshot) in
// no McDonalds appear so show a no results label
})
Since .childAdded only fires for existing child nodes that match the query, you can't use it to detect when there are no matches. To detect if there are no matches, you'll need to use a .value listener and check snapshot.exists.
You can either use that in addition to your existing .childAdded listener (the Firebase client deduplicates the requests behind the scenes, so no extra data will be transferred, or you can use the .value listener to handle the results too. In the latter case you will need to loop over snapshot.children to get to the individual result nodes.

FIRDataEventTypeValue and FIRDataEventTypeChildAdded firing order

The docs for the current Firebase Admin SDK as well as the 2.5.1 Firebase iOS SDK (now legacy) mentions some guarantees regarding events, the most relevant one to me being:
Value events are always triggered last and are guaranteed to contain updates from any other events which occurred before that snapshot was taken.
Does this still stand with respect to the iOS SDK (the current docs for which have since removed the table containing the guarantees), especially when interlacing observe and observeSingleEvent calls? In other words, if I were to call this code on app startup:
ref.child("users").observe(.childAdded) { snapshot in
print("Child added")
}
ref.child("users").observeSingleEvent(of: .value) { snapshot in
print("Value event")
}
Will I get a guarantee that the childAdded events are fired before the value event? In other words, will I get something this in the console, assuming there are 3 children under users?
Child added
Child added
Child added
Value event
More context:
I'm trying to load an initial blob of data from Firebase, after which I want to inform the application that it has received all initial data. In other words, I want to do something like this answer has suggested. Some simple experiments affirm that the guarantee is maintained, but this answer also suggests that interlacing observe and observeSingleEvent calls when local data is available will break the guarantee.

Firebase: how to retrieve only changed items next time app launches?

I've got an app that uses a Firebase db containing 100,000 items. My app has to process through each of these items which takes several seconds.
What is happening is that every time the app is launched (from a terminated state) those 100,000 items are being processed each time (even if the contents of the db on the Firebase server have not changed). Obviously, I don't want the app to do this if not necessary. Here's some code:
if dbRef == nil {
FirebaseApp.configure();
Database.database().isPersistenceEnabled = true
...
let dbRef = Database.database().reference(withPath: kFirebaseDBName)
_ = spamRef.observe(DataEventType.value, with: { (theSnapshot) in
if let content = theSnapshot.value as? [String : AnyObject]
{
self.processContent(content: content)
}
Each time the app is started then the content snapshot contains the entire database reference contents.
Is there a way of, for example, getting the last date the database was updated (on the server), or only obtaining the delta of changed items between each app launch - can a query return just changed since last queried for example, or something similar?
I don't know how many items have changed so cannot call something like:
queryLimited(toLast: N))
As I don't know what value N is.
I've tried adding keepSynced as follows in the hope it might change things, but no.
if dbRef == nil {
FirebaseApp.configure();
Database.database().isPersistenceEnabled = true
...
let dbRef = Database.database().reference(withPath: kFirebaseDBName)
dbRef.keepSynced(true)
_ = dbRef.observe(DataEventType.value, with: { (theSnapshot) in
if let content = theSnapshot.value as? [String : AnyObject]
{
self.processContent(content: content)
}
I have no idea how much data might have changed so don't know what value to supply to something like toLast or similar to modify the observation parameters.
The database (which was not created nor updated with new content by me) has 100,000 items in a flat structure (i.e. one parent with 100,000 children) and any number of these children in any order might have been deleted and replaced since last time my app ran, but the total will still be 100,000. None of the children have an explicit timestamp or anything like that.
I was under the impression if Firebase kept a local cache of the data (due to isPersistenceEnabled) then next time it connects with the server it would only sync what had changed on the server. Therefore in order to do this Firebase itself must internally have some delta information somewhere, so I was hoping that delta information may available in some form to my app.
Note: My app does not need persistence to be enabled, the above code is doing so just as variations to see if anything will result in the behavior I desire with the observer.
UPDATE
So looking at the documentation more you can set a timestamp for the last time a user was connected to the server using:
lastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())
Take a look at this question Frank explains some issues with persistence and listeners. The question is for Android but the principles are the same.
I still think the problem is your query. Since you already have the data persisted .value is not what you want since this returns all of the data.
I think you want to attach a .childChanged listener to your query. In this case the query will only return the data that has been changed. If you haven't heard of .childChanged before you can read about it here.
I didn't realize this problem is specifically related to persistence. I think you are looking for keepSynced(). Take a look at this.
ORIGINAL ANSWER
The problem is your query. You are asking for all of the data that's why you're getting all of the data. You want to look into limiting your queries using toFirst or toLast. Additionally, I don't think you can query for the last time the database was updated. You could check the last node in your data structure if you have the timestamp saved, but you might as well just get the newest data.
You want something like this:
ref.child("yourChild").queryLimited(toLast: 7).observeSingleEvent(of: .value, with: { snap in
// do something
})
Depending on how you're writing your data you'll want toLast or toFirst. Assuming the newest data is written last toLast is what you want. Also note that the numbers I am limiting to are arbitrary you can use any number that fits your project.
If you already have a key and you want to start querying above that key you can do something like this:
ref.child("YourChild").queryOrderedByKey().queryEnding(atValue: lastVisiblePostKey).queryLimited(toLast: 8).observeSingleEvent(of: .value, with: { snap in
// do something with more posts
})
You may also want to look into this question, this question and pagination.

Firebase observeEventType .childAdded doesn't work as promised

When I call this observe function from in my viewcontroller, the .childadded immediately returns a object that was already stored instead of has just bin added like .childadded would suspect.
func observe(callback: RiderVC){
let ref = DBProvider.Instance.dbRef.child("rideRequests")
ref.observe(DataEventType.childAdded) { (snapshot: DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
let drive = cabRide(ritID: ritID, bestemming: bestemming,
vanafLocatie: vanaf, taxiID: taxiID, status: status)
print(drive)
callback.alertForARide(title: "Wilt u deze rit krijgen?", message: "Van: \(vanaf), Naar: \(bestemming)", ritID: ritID)
}
}
}
When I try this function with .childchanged, I only get a alert when it is changed like it suppose to do, but when doing .chiladded, it just gets all the requests out of the database and those requests were already there.
When I add a new request, it also gives an alert. So it works, but how can I get rid of the not added and already there requests?
Does anybody know this flaw?
This is working exactly as promised. From the documentation:
Retrieve lists of items or listen for additions to a list of items.
This event is triggered once for each existing child and then again
every time a new child is added to the specified path. The listener is
passed a snapshot containing the new child's data.
That might seem weird at first, but this is generally what most developers want, as it's basically a way of asking for all data from a particular branch in the database, even if new items get added to it in the future.
If you want it to work the way you're describing, where you're only getting new items in the database after your app has started up, you'll need to do a little bit of work yourself. First, you'll want to add timestamps to the objects you're adding to the database. Then you'll want to do some kind of call where you're asking to query your database by those timestamps. It'll probably look something like this:
myDatabaseRef.queryOrdered(byChild: "myTimestamp").queryStarting(atValue: <currentTimestamp>)
Good luck!

With Firebase persistence, .childRemoved is not called when a child is removed*

I am running into a problem making sure data is synced with the database when using persistence. Before you run away I just have a question about a specific behavior, and I want to know if I am doing something wrong or if this is expected behavior.
Here is the basic setup I am using:
ref.observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
// Gets cached or live data
// Does all the things
ref.observe(.childAdded) { (snapshot: FIRDataSnapshot) in
// Gets new children added
// Adds the things
}
ref.observe(.childRemoved) { (snapshot: FIRDataSnapshot) in
// Gets removed children
// Removes the things
}
}
This works great in most use cases. When on the screen showing the data, in this case, a comments feed, The Data is added and removed perfectly.
More importantly, when coming back to a previously visited comment feed, the cache is loaded and then .childAdded is fired for every new comment that is not included in the cache. That's great.
The problem I am running into is when coming back to a previously visited comment feed where comments have been deleted while away. .childRemoved does not get invoked for each comment that has been deleted. Which is leaving stale data on the screen. I can see how this might be the expected behavior considering the data has been deleted and thus a snapshot is unavailable to be sent.
If that is the case, what would be the correct course of action to be sure a user is not presented a comment that has been deleted?
Let me know if any clarification is needed! Thank you.
I think what is happening here is a subtle race caused by the persistence behavior.
When you call observe, you set up a listener which will update as changes occur. When you call "observeSingleEvent", you generally do the same thing, but stop listening after the first result. However, with persistence enabled that first result will usually come from the cache. The call will, however, call a background update.
What I believe is happening here is:
Comment is deleted in comment feed while there are no listeners
The single event is attached, and the cached state including the deleted comment is returned
The comment feed is updated before the childRemoved listener is added
The childRemoved listener is added and does not fire
That said, I haven't repro'd this to validate for sure. One test though would be to setup the single event as a regular listener, and just cancel the listener one you receive one event. In connected states, that should prefer getting state from the backend, so you should see the comment feed with the comment deleted (the comment removed would never fire, but thats because the initial state would be coherent).

Resources