Firebase ChildAdded firing every time in my iOS App - ios

I have a simple iOS app that syncs 1,000 Contacts with my Firebase. persistenceEnabled is set to true in my AppDelegate.
In my UITableViewController I observe the ChildAdded event, download the Contacts and this works fine.
The problem is this - the next time I open the app, navigate back to the UITableViewController, the ChildAdded event fires again and loads the 1,000 Contacts.
What I thought would happen is that on subsequent launches the ChildAdded event would not fire since persistenceEnabled is true and no new Children have been added.
Have I misunderstood how Firebase works ?
ref.child("contacts").observeEventType(.ChildAdded, withBlock: { snapshot in
if let json = snapshot.value as? NSDictionary {
for key in json.keyEnumerator() {
if let dict = json.valueForKey(key as! String) as? NSDictionary {
let contact = Contact(data: dict)
contacts.append(contact)
}
}
}
})

From the Firebase documentation on .ChildAdded:
The FIRDataEventTypeChildAdded event is triggered once for each existing child and then again every time a new child is added to the specified path.
Using persistence doesn't make a difference to this behavior. It just ensure that the event will also work in this way if the app is started while you don't have network connectivity.

Since You are registering observeEventType not observeSingleEventType. So register event only once in viewDidLoad. For the first time you will get all the data available in that node. next time onwards it will give you only the new child added. In case if you want to query firebase every time then register observeSingleEventType but again it will return all the data available in that node and will remove observer as it is observeSingleEventType.

use this method :
Database.database().reference().child("anything").observeSingleEvent(of: .value, with: { snapshot in ...
this will fetch data once ..

Related

Tableview from Firebase Realtime database not always updating items

I am testing my app where I have included a tableview that shows items from a Firebase Realtime database.
Every time I create a new object from the app to Firebase, the tableview updates and include the new added object.
But I have detected that sometimes it doesn't maintain the items updated.
For example, if I add a new item from inside the app, it is automically included in the tableView.
If another user creates a new item from inside his app, it is also included and updated in both apps.
But I have detected that not all items are included in the tableview and if I edit an item directly in the firebase console,the app doesn't update inmediatelly the item in the tableview.
Here is the code I am using to populate the tableview:
databaseRef.child(codigo_chat).queryOrderedByKey().observe(.childAdded, with: { (snapshot) in
if let valueDictionary = snapshot.value as? [AnyHashable:String]
{
let texto_mensaje = valueDictionary["mensaje"]
let codigo_chat = valueDictionary["codigo_chat"]
let datetime = valueDictionary["datetime"]
let emisor = valueDictionary["emisor"]
let receptor = valueDictionary["receptor"]
self.mensajesSorted.insert(MisMensajes(codigo_chat: codigo_chat!, datetime: datetime!,emisor: emisor!, mensaje: texto_mensaje!, receptor: receptor! ), at: 0)
self.chatTV.reloadData()
}
})
Right now you're only observing the .childAdded event, which (as its name implies) only fires when a child is added to the location (and initially for all existing children).
If you also want to get called when a child is modified, you should also observe the .childChanged event. In that callback you can then update the existing UI for that child.
In the save vein there are two more events you may want to respond to:
.childRemoved, which is fired when a child is removed from the database, and you'll want to remove it from the UI.
.childMoved, which is fired when the child is moved to a different location in the query results, and which typically goes hand-in-hand with a .childChanged event. In your .childMoved code, you'll usually move the UI element for the child to its new location.
Also see the Firebase documentation on listening for child events.
You need to call reloadData in the main queue.
DispatchQueue.main.async {
self.chatTV.reloadData()
}

Firebase 'Observe' called multiple times with Swift on iOS

When I start the observer on a Firebase database node, I notice that Firebase continues to call the method observer even when there is no data change.
Here is my setup:
FIRDatabase
.database()
.reference(withPath: "test")
.observe(FIRDataEventType.value, with: { (snapshot) in
print("Firebase Data Updated");
}
);
When I make one change to the Firebase database, the observer calls its closure function more than one time.
Firebase Data Updated
Firebase Data Updated
Firebase Data Updated
Firebase Data Updated
Firebase Data Updated
Firebase Data Updated
Firebase Data Updated
...
Why does this occur?
How can I stop this from occurring and get only one call to the observer after an update?
It's likely this observer is being registered multiple times. When the user logs out, the listener block that you registered stays registered, such that when the user logs in again, you are registering a second listener.
It is often good practice to capture the ref and handle of observers, and remove the handles once you're done with them (i.e. when a user logs out). You can do so as such:
ref, handle = FIRDatabase
.database()
.reference(withPath: "test")
.observe(FIRDataEventType.value, with: { (snapshot) in
print("Firebase Data Updated");
}
);
And at sign out:
ref.removeObserverWithHandle(handle)
Otherwise another possible solution to ensure it is only called once is to use .observeSingleEvent() instead of .observe().
Swift 4:
If you want to remove all listeners registered with test node.
Database.database().reference(withPath: "test").removeAllObservers()
If you want to remove particular listener then you need particular handler for observer. You can use following sample code.
let ref = Database.database().reference(withPath: "test")
let handler = ref.observe(.value) { (snapshot) in
}
ref.removeObserver(withHandle: handler)

What's the right way to do an initial load of list data in Firebase and Swift?

Every firebase client example I see in Swift seems to oversimplify properly loading data from Firebase, and I've now looked through all the docs and a ton of code. I do admit that my application may be a bit of an edge case.
I have a situation where every time a view controller is loaded, I want to auto-post a message to the room "hey im here!" and additionally load what's on the server by a typical observation call.
I would think the flow would be:
1. View controller loads
2. Auto-post to room
3. Observe childAdded
Obviously the calls are asynchronous so there's no guarantee the order of things happening. I tried to simplify things by using a complete handler to wait for the autopost to come back but that loads the auto-posted message twice into my tableview.
AutoPoster.sayHi(self.host) { (error) in
let messageQuery = self.messageRef.queryLimited(toLast:25).queryOrdered(byChild: "sentAt")
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
if let dict = snapshot.value as? [String: AnyObject] {
DispatchQueue.main.async {
let m = Message(dict, key: snapshot.key)
if m.mediaType == "text" {
self.messages.append(m)
}
self.collectionView.reloadData()
}
}
})
}
Worth noting that this seems very inefficient for an initial load. I fixed that by using a trick with a timer that will basically only allow the collection view to reload maximum every .25s and will restart the timer every time new data comes in. A bit hacky but I guess the benefits of firebase justify the hack.
I've also tried to observe the value event once for an initial load and then only after that observe childAdded but I think that has issues as well since childAdded is called regardless.
While I'm tempted to post code for all of the loading methods I have tried (and happy to update the question with it), I'd rather not debug what seems to not be working and instead have someone help outline the recommended flow for a situation like this. Again, the goal is simply to auto-post to the room that I joined in the conversation, then load the initial data (my auto-post should be the most recent message), and then listen for incoming new messages.
Instead of
self.newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) in
try replacing with
let childref = FIRDatabase.database().reference().child("ChildName")
childref.queryOrdered(byChild:"subChildName").observe(.value, with: { snapshot in

In Firebase, retrieved data doesn't get out from the .observeEventType method

I defined a class in a project, to manage my database set up in Firebase. The following is what I've done with the class so far.
import Foundation
import Firebase
class db{
class func getPrim() -> [String]{
var ret = [String]()
let ref = FIRDatabase.database().reference()
ref.child("bunya1").observeEventType(FIRDataEventType.Value, withBlock: {
s in
ret = s.value! as! [String]
})
print("ret: \(ret)")
return ret
}
}
And the method is called in a print() method, like print(db.getPrim()). But what the console(or terminal? anyway the screen on the bottom of xcode..) says is only an empty array. I embraced the statement above with print("-----------------------").
-----------------------
ret: []
[]
-----------------------
2016-09-07 20:23:08.808 이모저모[36962:] <FIRAnalytics/INFO> Successfully created Firebase Analytics App Delegate Proxy automatically. To disable the proxy, set the flag FirebaseAppDelegateProxyEnabled to NO in the Info.plist
2016-09-07 20:23:08.815 이모저모[36962:] <FIRAnalytics/INFO> Firebase Analytics enabled
Seems like ret in .observeEventType() method does not take its value out of the method block. As far as I know the data is supposed to be kept.. Can anyone give me a hint? I still don't understand how the code block as a method parameter works. Thnx!!
All firebase operations are by definition asynchronous which means your program doesn't wait for the data from firebase before going to the next statement in your code. So by the the time your print statements are called the data from firebase hasnt been fetched yet.
Take a look at this answer for more information.
André (and the link to Vikrum's answer) are indeed why this is happening. But it's usually easiest to understand if you add a few log statements to your code:
class func getPrim() -> [String]{
let ref = FIRDatabase.database().reference()
print("Before observer");
ref.child("bunya1").observeEventType(FIRDataEventType.Value, withBlock: {
s in
print("In observer block");
})
print("After observer");
return "..."
}
When you run this code, the logging will be in this order:
Before observer
After observer
In observer callback
This is probably not the order in which you expected them to appear. But it definitely explains why your returning an empty array in your snippet. If the code inside the block hasn't run yet, the item hasn't been added to the array yet.
The reason this order is inverted is as André and Vikrum say: the call to Firebase happens asynchronously. Since it can take some time (especially if this is the first time you're accessing the database), the Swift code continues executing to ensure the app stays responsive. Once the data comes back from Firebase, your block is called (for that reason it's sometimes referred to as a "callback") and you get the data.

FEventType.ChildAdded event only fired once

my firebase data structure looks like the following
user
|__{user_id}
|__userMatch
|__{userMatchId}
|__createdAt: <UNIX time in milliseconds>
I'm trying to listen for the child added event under userMatch since a particular given time. Here's my swift code:
func listenForNewUserMatches(since: NSDate) -> UInt? {
NSLog("listenForNewUserMatches since: \(since)")
var handle:UInt?
let userMatchRef = usersRef.childByAppendingPath("\(user.objectId!)/userMatch")
var query = userMatchRef.queryOrderedByChild("createdAt");
query = query.queryStartingAtValue(since.timeIntervalSince1970 * 1000)
handle = query.observeEventType(FEventType.ChildAdded, withBlock: { snapshot in
let userMatchId = snapshot.key
NSLog("New firebase UserMatch created \(userMatchId)")
}, withCancelBlock: { error in
NSLog("Error listening for new userMatches: \(error)")
})
return handle
}
What's happening is that the event call back is called only once. Subsequent data insertion under userMatch didn't trigger the call. Sort of behaves like observeSingleEventOfType
I have the following data inserted into firebase under user/{some-id}/userMatch:
QGgmQnDLUB
createdAt: 1448934387867
bMfJH1bzNs  
createdAt: 1448934354943
Here are the logs:
2015-11-30 17:32:38.632 listenForNewUserMatches since:2015-12-01 01:32:37 +0000
2015-11-30 17:45:55.163 New firebase UserMatch created bMfJH1bzNs
The call back was fired for bMfJH1bzNs but not for QGgmQnDLUB which was added at a later time. It's very consistent: after opening the app, it only fires for the first event. Not sure what I'm doing wrong here.
Update: Actually the behavior is not very consistent. Sometimes the call back is not fired at all, not even once. But since I persist the since time I should use when calling listenForNewUserMatches function. If I kill the app and restart the app, the callback will get fired (listenForNewUserMatches is called upon app start), for the childAdded event before I killed the app. This happens very consistently (callback always called upon kill-restart the app for events that happened prior to killing the app).
Update 2: Don't know why, but if I add queryLimitedToLast to the query, it works all the time now. I mean, by changing userMatchRef.queryOrderedByChild("createdAt") to userMatchRef.queryOrderedByChild("createdAt").queryLimitedToLast(10), it's working now. 10 is just an arbitrary number I chose.
I think the issue comes from the nature of time based data.
You created a query that says: "Get me all the matches that happened after now." This should work when the app is running and new data comes in like bMfJH1bzNs. But older data like QGgmQnDLUB won't show up.
Then when you run again, the since.timeIntervalSince1970 has changed to a later date. Now neither of the objects before will show up in your query.
When you changed your query to use queryLimitedToLast you avoided this issue because you're no longer querying based on time. Now your query says: "Get me the last ten children at this location."
As long as there is data at that location you'll always receive data in the callback.
So you either need to ensure that since.timeIntervalSince1970 is always earlier than the data you expect to come back, or use queryLimitedToLast.

Resources