Some questions about keepSynced(true) on a limited query reference - ios

I have an Firebase-backed app with the following database structure:
posts
/uid
/postId
Originally, i'd load data from the posts/uid node using ObserveEventOfType with .childAdded. This would load stale data frequently (~5 times a day) for all users of my app simutaneously. When attempting to update the data by making a new post, Firebase would still return stale data.
As a result, I decided to try keepSynced. Now, if my reference looked like this:
reference = Database().database.reference.child("posts").child(uid)
keepSynced would load all of the data at that node, which could amount to very large downloads if there are many children in that node. So, I decided to change the reference/query to:
reference = Database().database.reference.child("posts").child(uid).queryLimited(toLast: 25)
When turning keepSynced on for this node, it syncs for the last 25 children in the node successfully. However, I still am facing the issue of receiving stale data rather frequently. So here are my questions:
When adding the keepSynced mode on the limited query, does it only sync from the initial node you added it to, or does it always just sync the 25 latest children under that node?
Where is the best place to add the keepSynced(true) line in code? Before we load the reference, in viewWillAppear, or inside of the actual download callback?
Similarly, where is the best place to use keepSynced(false)?
Do the keepSynced listeners delete when the app fades into the background?
Why does keepSynced sometimes not address for child updates?
I currently use keepSynced(true) inside of the function I use to load posts which is called on viewDidLoad.
Thanks in advance.

As its name implies keepSynced(true) keeps whatever query or reference you call it on synchronized in the local cache. It quite literally just attaches an empty observer to that query/reference. So in your Database().database.reference.child("posts").child(uid).queryLimited(toLast: 25) it will sync the last 25 child nodes, and keep synchronizing those (removing previous ones as new ones are added).
Firebase Realtime Database caching mechanism works most reliably if you repeatedly listen for the exact same data. Specifically, a .value listener attached to Database().database.reference.child("posts").child(uid) may not see data that was cached through Database().database.reference.child("posts").child(uid).queryLimited(toLast: 25). This is because the Firebase client guarantees to never fire events for partial updates, and in this example it can't guarantee that it has all data from the first reference.
For your questions:
See above...
It's most common to add them in viewWillAppear.
I'm not sure why you'd want to call keepSynced false, so can't recommend anything there.
Not sure if this is what you mean, but keepSynced(true) is not persisted between runs of the app. So you have to call keepSynced(true) each time your app/view starts.
See above...
In general you seem to try and work around the way the API works, by calling the API in different ways. I typically don't see great results from that. If you want your app to behave differently than the API does, consider creating a custom wrapper, and caching the data there.

Related

Firebase .observeSingleEvent(of:with:) method is retrieving cached/old data [duplicate]

This question already has answers here:
Firebase Offline Capabilities and addListenerForSingleValueEvent
(4 answers)
Closed 4 years ago.
[Disclaimer] I have personally posted and answered this question after having struggled with it myself and, relevantly, noticed that many people still do
Context
I am developing an iOS mobile application and - for this particular project - decided to use the Firebase Realtime Database as my backend infrastructure.
Problem
When querying data at a specific node using the .observeSingleEvent(of:with:) method, I always find myself retrieving either cached or old data rather than the newly updated one.
In some cases, calling the method twice in a row retrieves the desired server data.
Attempts
Used .keepSynced(true) at the relevant node which, according to the Firebase documentation
automatically downloads the data at these locations and keeps it in sync even if the reference has no active listeners
Overview
Going through the documentation, you notice that there are two primary ways of querying data from the Firebase Realtime Database into your iOS mobile application
The .observe(_:with:) method which, according to the Firebase Documentation, continuously listens for changes at a particular node and triggers the callback every time the data changes at the latter.
This method is triggered once when the listener is attached and again every time the data, including any children, changes. The event callback is passed a snapshot containing all data at that location, including child data. If there is no data, the snapshot will return false when you call exists() and nil when you read its value property.
The .observeSingleEvent(of:with:) method which, according to the Firebase Documentation, is called exactly once.
In some cases you may want a callback to be called once and then immediately removed, such as when initializing a UI element that you don't expect to change. You can use the observeSingleEventOfType method to simplify this scenario, [in which] the event callback [is triggered] once and then does not trigger again.
Problem
After going through the different possible methods of querying your data, you've realized that the .observeSingleEvent(of:with:) method suits better your current database-reading needs. However, implementing it in your application keeps on retrieving cached and old data no matter how many times you modify your database. You've called the .keepSynced(true) at the relevant database reference, yet in vain. You've opted for the .observe(_:with:) method instead, and everything starts to work perfectly fine.
So what might be the issue?
Solution
The reason you might be going through this problem is perfectly logical if you have invalid database security rules. These can easily prevent you from retrieving your desired data and synchronizing your realtime database.
Let's assume you are trying to synchronize the myRef database reference. You need to set the correct rules that allow reading from this database reference - something along the lines of ".read" = true".
[Warning] Please be careful with these database security rules. Incorrect rules can lead to drastically undesired behaviors, such as people illegally reading and/or writing from/into your database. A good video on how to set flawless security rules is The key to firebase security - Google I/O 2016

Firebase Database - Avoid downloading JSON repeatedly on iOS

My use-case for Firebase is slightly different than most. We do not use FB exclusively for our back-end. We have a large MariaDB server dealing with relations and all data.
Our goal with FB is to allow clients on iOS devices to have their specific data available. We need to load the data once and then listen for changes to this particular data. Here is a rough overview of how it works:
The main ViewController is loaded
Firebase is initialized and we listen for FIRDataEventTypeChildAdded. (Persistence is enabled)
Firebase loads all matching records. We then loop through and store them locally in the internal SQLite DB.
In the normal userflow, we push other ViewControllers on the screen. The problem is, once the main ViewController is loaded, FIRDataEventTypeChildAdded fires again for each record.
Questions:
When FIRDataEventTypeChildAdded fires again, is it loading the data from its internal cache (Persistence?) or is it re-downloading everything from the Firebase server? I've used Network Link Conditioner to completely cut the internet connection, and when I do, it does not fire the FIRDataEventTypeChildAdded at all, but as soon as the net comes back, it fires FIRDataEventTypeChildAdded for every single record.
How can I achieve the above where we load all records on login and then only listen for changes to those records? I am already using orderBy and startingAt so if the answer involves one of the above, I cannot add another "hasDownloaded=yes" filter.
Thanks in advance.
A Firebase reference listener connects to the server once, and stays connected until that query is turned off. As long as the reference being listened to is in memory, there is only one connection made to the database. Once this connection happens, all data will come through as child added data again.
The issue here is not so much with Firebase but that your app is continuously readding listeners to a reference, making the data be redownloaded from the network every time.
So to your first question, yes it is redownloading from the network. To your second, you just need to make sure the Firebase query never leaves memory. This can be done by making your query globally scopes, or simply by not turning off the query when the view controller exits scope (then you need to make sure not to readd multiple queries on subsequent loads).

Firebase observing adding new records

Before you link me to a duplicate, please read what I'm asking..
I'm building an app which basically has a list of about 5000 teams. These teams are fairly static (they don't change very often). I would like to observe any time one is changed though as it's essential it get's updated in the app ASAP.
If I include dbTeams.ref.observe(.childAdded, with: {}), it runs each time the app starts, loading over all 5000 records despite having them in the persistent storage already (I have enabled persistence).
Now the documentation says this will happen, I know, but with 5000 records (and potentially way more in the future), I can't have this happen.
My options so far (from what I've found and tried) are:
Add a timestamp to each record and create a custom query to call .childAdded after the last timestamp... This is inefficient. Storing a timestamp for soccer teams which will hardly ever change, is silly. It also means keeping a copy of the last time it was checked.
Create a sub-list within the Teams list. This too is silly as you may as well call .value and get the whole bunch of data in one go.
Just live with it... Fine - until it scales to tens of thousands of records. Not clever either.
It just seems weird that all the other event listeners only fire when they are "supposed to" except this one.
Any help would be appreciated - how do I achieve what I need?

Persist offline changes separately from original data in Core Data

I'm in the middle of adding an "offline mode" feature to an app I'm currently working on. Basically the idea is that users should able to make changes to the data, for example, edit the description of an item, without being connected to the internet, and the changes should survive between app launches.
Each change would normally result in an API request when working online but situation is different in offline mode.
Right now this is implemented by storing all data coming from the API in a Core Data database that acts as a cache. Entities that can be edited by user in addition to normal attributes have the following ones:
locallyCreated - whether the object was created offline
locallyDeleted - object was deleted offline
locallyUpdated - updated
This makes it possible to look for new/deleted/updated objects and send corresponding API requests when doing sync.
This worked well for creating and deleting objects, however, one disadvantage I found with this approach is when new data is retrieved from the API all local changes (i.e. attributes of objects marked as locally updated) are lost, which means that they have to be stored separately somehow.
What would be the best way to approach this problem?
Since you have your locallyUpdated key, the obvious answer is to modify your code that imports server changes, so that it doesn't overwrite changes to any object marked as changed. One way or another you need to avoid overwriting those changes, and you're already keeping a record of which objects have changes, so you already have the tools for a basic solution.
But you'll soon run into the complexity of syncing data. What if the local object has changes on one key, but the incoming data from the server has changes on a different key? You can't resolve that just by knowing that the local copy has changed somehow. Maybe you decide that the server always wins, or that the local copy always wins. Those are easy, if they make sense for your app. If you need to merge changes though, you have some work ahead of you. You would need to record not only a Boolean value indicating that changes were made, but also a list of which keys had changed. This can get complicated, but it's the nature of data syncing.

Retrieve data/snapshot after manually remove children in Firebase

I have a function that uses .observeSingleEventOfType with .Value to retrieve data from a subtree from Firebase. The issue I'm facing right now is that, every single time I manually remove children/data in Firebase to test how this function behaves, it always loads the old data the first time when it gets called after deletion. After the first time, it then loads the correct data.
I have tried to use .ChildAdded and .observeEventType, but the behavior didn't change. I have Firebase.defaultConfig().persistenceEnabled set to true now, and I am guessing the problem is that snapshot reads data from cache if it's available, otherwise checks Firebase database.
Any one experiences this kind of issues before?
Any help would be highly appreciated.
UPDATE:
I have tested with and without persistence enabled, it turns out that I was correct, when Firebase.defaultConfig().persistenceEnabled caches all data in the past to memory, and the observe functions will try to load from memory first then go to Firebase.
When I set Firebase.defaultConfig().persistenceEnabled = false The problem went away. However, I would like to have my app work offline as well which mean I do need to set Firebase.defaultConfig().persistenceEnabled = true Is there a way to have all observe functions cache data except this specific one?
SOLUTION:
The problem is fixed now, the issue was that I didn't remove observers when the view controller was dismissed. So every time I enter the view again, another observer gets called and thus I always receive more than one call backs.

Resources