keepSynced(true) doesn't fetch the data for the location immediately - ios

As the title mentioned, I get local data with keepSynced(true) and have to go out and back into the view to get the latest (fresh) data.
i have the following setup:
var baseRef: FIRDatabaseReference!
in viewDidLoad:
baseRef = FIRDatabase.database().reference()
baseRef.child("Posts").keepSynced(true)
then in viewWillAppear:
baseRef.child("Posts").queryOrderedByChild("sortTimestamp").observeSingleEventOfType(.Value, withBlock: { snapshot in //etc
I get local data, according to this post the data should be fresh as I keepSynced before I attach an observer:
https://groups.google.com/forum/#!searchin/firebase-talk/keepSynced/firebase-talk/SK9WVZvkHsU/HVjxtnv5JoYJ
"keepSynced will work fine both with disk persistence enabled and with it disabled. It immediately fetches the data for the location/query where you enable it, before you attach a listener"
However, as mentioned, I have to trigger this again, by going out and in of the viewController(almost like a refresh), to get fresh data
I did try the normal observeEventType(.Value as well as .ChildAdded, but with the pagination system I use observeSingleEventOfType works better except for the issues above
Not sure what I am missing, any suggestions are much appreciated.

Related

Firestore & SwiftUI - is my snapshot reader reading the db needlessly?

I have a project with firebase set up, and I have a function set up similarly to the sample from firebase, but the firebase document updates #Published vars in my observable object:
func getDataFromSession(env: GlobalEnvironmentObject) {
db.collection("sessions").document("firstSession")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("Current data: \(data)")
env.data1 = data[data1]
// env.data1 is the #Published bar data1
}
}
and I'm calling this function in my swiftui view :
var body: some View {
let fbConnection = FirebaseConnectionHandler(env: env)
fbConnection.getDataFromSession()
When I run my app, my debug window is executing the print("Current data: (data)") line at lightning speed despite the fact that no values in the observable object/global environment have change. I was under the impression that the snapshot listener will only perform a read when data changes, and the swiftui views will only update when #Published properties change and so fare neither are happening (so it shouldn't be reading like crazy). And I'm not sure if it's performing a ton of reads in a row or if that output is normal (obv I only want it to read when there is a change in my document)...
What the heck am I doing wrong? How do I prevent the mega reads and have it so the data only reads when it is updated?
Welcome to Stackoverflow. Relax, you and the SDK are doing it nothing wrong. The very first read you are experiencing is due to the initial state of your data.
See the doc View changes between snapshots :
https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots
Important: The first query snapshot contains added events for all
existing documents that match the query. This is because you're
getting a set of changes that bring your query snapshot current with
the initial state of the query. This allows you, for instance, to
directly populate your UI from the changes you receive in the first
query snapshot, without needing to add special logic for handling the
initial state.
The initial state can come from the server directly, or from a local
cache. If there is state available in a local cache, the query
snapshot will be initially populated with the cached data, then
updated with the server's data when the client has caught up with the
server's state.
Ok, so I figured it out...if you are updating observable objects from firebase, put the listener object in your view (under "var body: some View {")...
Then, take any of the views in your struct (HStack, ZStack, etc) and add a .onAppear modifier, and call the function the updates your firebase from there...
Turns out the view keeps updating, but .onAppear will kick off the refreshing once...
So, it turns out when I was writing my data to Firestore, all my data was being written to the same document. And, after searching the web, i learned that, when it comes to listeners in firestore, it is an all or nothing thing. So, my entire document was being called and updated, which triggered updates elsewhere, that would call and trigger, etc etc etc.
To fix, I took the document and split it into two documents, where one app can read to one but only write to the other, and the other app did vice versa (read to the other, write to the other). This way, the listener calls changes on a per document level and avoids the looping...

Update Firebase local data with observeSingleEvent

so I have a function to retrieve the user information from a given user id:
func getUserDataFrom(_ userID: String, completion: #escaping (_ userData: DBUser) -> Void) {
ref.child(usersTable).child(userID).observeSingleEvent(of: .value) { (snapshot) in
if let userDic = snapshot.value as? NSDictionary {
let userData = DBUser(with: userDic)
completion(userData)
}
}
}
The problem is that this returns the local data instead of reading from Firebase. I'd like to retrieve the data from the server (as long as there's internet connection) and only read from disk if it's not available.
I know that the easiest way to accomplish this would be using a listener, but I'm making a Today Extension and they use way too much memory increasing the chances of a crash.
I've also researched about keepSynced feature but since the database reference to the users table will have a lot of children I don't know if this will affect the memory of my extension.
Long story short: I'd like to read data from Firebase once, and only read from disk if there isn't internet connection with the minimum memory usage possible.
Thank you in advance.
I retrieve some explanation, I think it might help you in your case :
ObserveSingleEventType with keepSycned will not work if the Firebase
connection cannot be established on time. This is especially true
during appLaunch or in the appDelegate where there is a delay in the
Firebase connection and the cached result is given instead. It will
also not work at times if persistence is enabled and
observeSingleEvent might give the cached data first. In situations
like these, a continuous ObserveEventType is preferred and should be
used if you absolutely need fresh data.
I think you don't have the choice to use a continuous listener. But to avoid performance issues why you don't remove yourself your listeners when you don't it anymore.
In the fresh project I created and added your code, it retrieves data from Firebase when there's a connection and when not, from local storage. Because of that, we conclude the above code is correctly fetching Firebase data from their server.
However, in my experience observeSingleEvent and offline persistence has been a tad intermittent (perhaps a 'feature'?). To fix it, force the data at the reference to stay sync'd
let usersTableRef = Database.database().reference(withPath: usersTable)
let thisUsersTableRef = usersTableRef.child(userId)
thisUsersTableRef.keepSynced(true)
//optional: thisUsersTableRef.child("temp").setValue(true)
thisUsersTableRef.observeSingleEvent(of: .value)
See Offline Capabilities for a bit more info and further examples.
Also see this post from 2015 for some insight on observers/listeners.

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

Firebase started in offline mode, wont fire osberve* for child values

I'm using firebase with app started in offline mode, when i'm subscribing to child values of some node the callback from observe*(_:,withBlock:) is not firing (neither for initial values nor changes). Subscriptions to direct values (childless) works fine. Take a look at a snippet :
let database = FIRDatabase.database()
database.reference().keepSynced(true)
let databaseRef = database.reference()
database.goOffline()
databaseRef.child("user").setValue("user1")
let userKey = databaseRef.child("usr").childByAutoId().key
let userValues = ["uid": "uid",
"name" : "name",
"surname" : "surname"]
databaseRef.child("/usr/\(userKey)/").setValue(userValues)
//1
databaseRef.child("user").observeSingleEventOfType(.Value, withBlock:{ snap in
print("works")
})
//2
databaseRef.child("usr").observeSingleEventOfType(.Value, withBlock:{ snap in
print("doesnt work")
})
//3
databaseRef.child("usr/\(userKey)/uid").observeSingleEventOfType(.Value, withBlock:{ snap in
print("works")
})`
subscriptions 1 & 2 works fine, but subscribtion 2 won't fire, until at least once database go online. From the moment database syncronize with remote i can go offline and everything works as it should. Anyone know how to handle this issue?
When your app is offline, the Firebase client will fire events from its cache. If your app has never connected to the Firebase servers, this cache will be empty.
That means that the Firebase client has no knowledge on whether a value exists at the location you request. For that reason it will not fire an event.
I had a similar issue but I was using deep links with updateChildValues, which somehow caused the local cache to not fire events on intermediate (/path/intermediate/otherpath) keys. The workaround I found was to be more verbose in the dictionary I passed to updateChildValues. (I still believe this is a bug in the Firebase SDK).
See this Stack Overflow question

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