Firebase observe by value fetches old value from cached data - ios

I have been facing some issues with firebase persistence once enabled, I had a chance to read through the rest of posted questions and reviewed there answers but still haven't got things to work as expected.
I have enabled firebase persistence and using observe by value to fetch recent update of particular node. Not only it keeps fetching old values but also once I leave a particular view controller and go back to that view controller the value changes to recent one.
Is there a proper way to request for recent value at first call?
Code I have tried:
// MARK: Bill authenticate function
func authenticateBill(completion: #escaping (_ bill: Double?, _ billStatus: BillError?) -> Void) {
// Observe incase bill details exist for current case
let billRef = self.ref.child("bills").child((caseRef?.getCaseId())!)
billRef.observe(FIRDataEventType.value, with: { (billSnapshot) in
if !billSnapshot.exists() {
completion(nil, BillError.unavailable)
return
}
if let billDictionary = billSnapshot.value as? [String: AnyObject] {
let cost = billDictionary["cost"] as! Double
print("Cost: ", cost)
completion(cost, nil)
}
})
}

No, with persistence enabled, your first callback will contain the immediately-available value from the cache (if any), followed by updates from the server (if any). You can't indicate to the SDK that you want either cached or fresh data.
If you want to keep a particular location up to date all the time while persistence is enabled, you can use keepSynced to indicate to the SDK it should always be listening and caching the data at that location. That comes at an indeterminate cost in bandwidth, depending on how frequently the data changes.
You can use the REST API to request fresh data without going through any caching mechanisms.

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...

How to handle first time launch experience when iCloud is required?

I am using CloudKit to store publicly available data and the new NSPersistentCloudKitContainer as part of my Core Data stack to store/sync private data.
When a user opens my app, they are in 1 of 4 states:
They are a new user with access to iCloud
They are a returning user with access to iCloud
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
States 1 and 2 represent my happy paths. If they are a new user, I'd like to seed the user's private store with some data before showing the initial view. If they are a returning user, I'd like to fetch data from Core Data to pass to the initial view.
Determining new/old user:
My plan is to use NSUbiquitousKeyValueStore. My concern with this is handling the case where they:
download the app -> are recorded as having launched the app before -> delete and reinstall/install the app on a new device
I assume NSUbiquitousKeyValueStore will take some time to receive updates so I need to wait until it has finished synchronizing before moving onto the initial view. Then there's the question of what happens if they don't have access to iCloud? How can NSUbiquitousKeyValueStore tell me if they are a returning user if it can't receive the updates?
Determining iCloud access:
Based on the research I've done, I can check if FileManager.default.ubiquityIdentityToken is nil to see if iCloud is available, but this will not tell me why. I would have to use CKContainer.default().accountStatus to learn why iCloud is not available. The issue is that is an asynchronous call and my app would have moved on before learning what their account status is.
I'm really scratching my head on this one. What is the best way to gracefully make sure all of these states are handled?
There's no "correct" answer here, but I don't see NSUbiquitiousKeyValueStore being a win in any way - like you said if they're not logged into iCloud or don't have network access it's not going to work for them anyway. I've got some sharing related stuff done using NSUbiquitiousKeyValueStore currently and wouldn't do it that way next time. I'm really hoping NSPersistentCloudKitContainer supports sharing in iOS 14 and I can just wipe out most of my CloudKit code in one fell swoop.
If your app isn't functional without cloud access then you can probably just put up a screen saying that, although in general that's not a very satisfying user experience. The way I do it is to think of the iCloud sync as truly asynchronous (which it is). So I allow the user to start using the app. Then you can make your call to accountStatus to see if it's available in the background. If it is, start a sync, if it's not, then wait until it is and then start the process.
So the user can use the app indefinitely standalone on the device, and at such time as they connect to the internet everything they've done on any other device gets merged into what they've done on this new device.
I struggled with this problem as well just recently. The solution I came up with was to query iCloud directly with CloudKit and see if it has been initialized. It's actually very simple:
public func checkRemoteData(completion: #escaping (Bool) -> ()) {
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Root'")
let query = CKQuery(recordType: .init("CD_Container"), predicate: predicate)
db.perform(query, inZoneWith: nil) { result, error in
if error == nil {
if let records = result, !records.isEmpty {
completion(true)
} else {
completion(false)
}
} else {
print(error as Any)
completion(false)
}
}
}
This code illustrates a more complex case, where you have instances of a Container entity with a derived model, in this case called Root. I had something similar, and could use the existence of a root as proof that the data had been set up.
See here for first hand documentation on how Core Data information is brought over to iCloud: https://developer.apple.com/documentation/coredata/mirroring_a_core_data_store_with_cloudkit/reading_cloudkit_records_for_core_data
to improve whistler's solution on point 3 and 4,
They are a new user but do not have access to iCloud for some reason
They are a returning user but do not have access to iCloud for some reason
one should use UserDefaults as well, so that it covers offline users and to have better performance by skipping network connections when not needed, which is every time after the first time.
solution
func isFirstTimeUser() async -> Bool {
if UserDefaults.shared.bool(forKey: "hasSeenTutorial") { return false }
let db = CKContainer.default().privateCloudDatabase
let predicate = NSPredicate(format: "CD_entityName = 'Item'")
let query = CKQuery(recordType: "CD_Container", predicate: predicate)
do {
let items = (try await db.records(matching: query)).matchResults
return items.isEmpty
} catch {
return false
// this is for the answer's simplicity,
// but obviously you should handle errors accordingly.
}
}
func showTutorial() {
print("showing tutorial")
UserDefaults.shared.set(true, forKey: "hasSeenTutorial")
}
As it shows, after the first time user task showTutorial(), UserDefaults's bool value for key "hasSeenTutorial" is set to true, so no more calling expensive CK... after.
usage
if await isFirstTimeUser() {
showTutorial()
}

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

Call ExtensionDelegate to create/refresh data for Complication

All my data creation is done in the ExtensionDelegate.swift.
The problem is ExtensionDelegate.swift doesn't get called before the function getCurrentTimelineEntryForComplication in my ComplicationController.swift.
Any ideas? Here is my code and details:
So my array extEvnts is empty in my ComplicationController.swift:
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
let extEvnts = ExtensionDelegate.evnts
}
Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array:
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
private let session = WCSession.defaultSession()
var receivedData = Array<Dictionary<String, String>>()
static var evnts = [Evnt]()
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TeamColor"] as? String, let matchValue = userInfo["Matchup"] as? String {
receivedData.append(["TeamColor" : tColorValue , "Matchup" : matchValue])
ExtensionDelegate.evnts.append(Evnt(dataDictionary: ["TeamColor" : tColorValue , "Matchup" : matchValue]))
} else {
print("tColorValue and matchValue are not same as dictionary value")
}
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if WCSession.isSupported() {
session.delegate = self
session.activateSession()
}
}
}
EDIT:
Per Apple, it looks like this has something to do with it, but for some reason I have no idea how to actually implement it because I'm not able to call mydelegate.evnts:
// Get the complication data from the extension delegate.
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
var data : Dictionary = myDelegate.myComplicationData[ComplicationCurrentEntry]!
So I've tried something like this, and still can't get it working because I'm still getting no data:
func someMethod() {
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
let dict = ExtensionDelegate.evnts
print("ExtensionDel.evnts: \(dict.count)")
}
Useful question that helped me here
In the function requestedUpdateDidBegin() you can update the information that you will display in your complication. So in this method you may make a call to your parent app using a WatchConnectivity method like sendMessage:replyHandler:errorHandler: to receive new information.
You can use NSUserDefaults to store your imperative data that will be used in your ComplicationController, then load this information from NSUserDefaults for your complication. I store this data in user defaults so that I always have old data to display in case the new data fails to load.
TL/DR: Have the extension tell ClockKit to update the complication after the data is received.
First issue:
So my array extEvnts is empty in my ComplicationController.swift ... Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array
Your array is empty because the data hasn't been received at that point.
You can't (get the complication controller to) force the watch (extension) to receive data which may not have even been transmitted yet.
If you look at the WCSession Class Reference, transferUserInfo queues data to be transferred in the background, when the system decides it's a good time to send the info.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
Second issue:
You're trying to combine updating your app and your complication based on data sent from your phone. But your app and your complication don't necessarily run together. It's not surprising or unexpected that the watch updates the complication before any data has even been sent/received. The App Programming Guide for watchOS mentions that
Complications exist entirely in the WatchKit extension. Their user interface is not defined in the Watch app. Instead, it is defined by an object implementing the CLKComplicationDataSource protocol. When watchOS needs to update your complication, it launches your WatchKit extension. However, the Watch app’s executable is not launched.
There's no mechanism for the complication controller to say, "Wait, I'm not ready to provide an update. The complication controller can't wait on (or as mentioned, force) the watch extension to receive data.
It's only responsibility is to immediately return data based on what's currently available to it. If there's no data, it must return an empty timeline.
Approaching this problem:
You shouldn't necessarily think of app updates and complication updates as the same thing. The first is not budgeted, but the second is budgeted. If you update your complication too often, you may exceed your daily budget, and no further updates will occur for the remainder of the day.
Complications aren't meant to be frequently updated. Complications should provide as much data as possible during each update cycle. You shouldn't ask the system to update your complication within minutes. You should provide data to last for many hours or for an entire day.
Having covered that, you could wait until your extension has received data, then can ask ClockKit to extend your timeline, so new entries can be added to it. extendTimelineForComplication: is documented in the CLKComplicationServer Class reference.
As an aside, if your data is urgent, you should use transferCurrentComplicationUserInfo. It's a high-priority message, which is placed at the head of the queue, and the extension is woken up to receive it. See this answer for a comparison between it and transferUserInfo.
You also could setup a singleton to hold your data which the watch app and complication controller both use. This was mentioned in an answer to an old question of yours, and also recommended by an Apple employee on the developer forums.

Resources