So I've been running into a bizarre problem. Essentially, I have an array which is the data source for a tableView:
data : [(String: SomeDataStruct)] = [(username1, data1), (username2, data2), (username3, data3)...]
In my firebase database, my data is:
If I delete a value from the tableview, this is done by deleting the corresponding item from the array. I then delete the item from firebase to keep the data in sync. For example if I want to delete username2, I remove the tuple with username2 from the array - it's at index 1. Then I use:
data : [(String: SomeDataStruct)] = [(username1, data1), (username2, data2), (username3, data3)...]
referenceToOrders.child("username2").removeValue(completionBlock: { (err, ref) in
print(data)
// data is [] when it should be [(username1, data1), (username3, data3), (username4 ... ]
})
I've isolated it down to this one line - for some reason invoking the firebase removeValue function results in the data array getting wiped out. As a workaround, before invoking the call, I create a copy of the data array. However, I'm not happy with this solution.
Help?
Answering for future reference and others.
So after adding a lot of print statements, I found out that the issue was that I had a observe Firebase handler that ran right after the removeValue but before the completion handler. Makes sense because the database has changed at this point, so the handler fires.
Related
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...
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.
I have been using firestore in iOS Swift 4 for last few weeks to build a simple demo app as alternative to realm. It has a simple table view that gets populated and kept in sync across devices as user does CRUD operations
In my app - I have added a snapshotlistener to a query
self.changeListener = query.addSnapshotListener { [weak self](queryResultSnapshot, error) in
//process document changes
}
In the callback - I have handled the added, updated, deleted changes based on DocumentChangeType present in queryResultSnapshot.changes.
Main problem is when I delete a document using
reference.delete(completion:)
After the delete is successful - I see the following events received in my querySnapshotListener.
//following is a debug message printed in delete function to correlate document ID
Will delete reference: i0W76CZP5X41vRp6BmzY
//following 3 are printed in the snapshot change listener
Deleted reference: i0W76CZP5X41vRp6BmzY, Source of change: Server
Created reference: i0W76CZP5X41vRp6BmzY, Source of change: Server
Deleted reference: i0W76CZP5X41vRp6BmzY, Source of change: Server
In the above print - i'm using the pending writes flag to print the source of change also. As we can see - when i do a delete - I get a delete notification, immediately followed by a additional create / delete of same document reference.
Does anyone else see this behavior? I did not see this behavior till couple of days back - so I'm curious if there is something that i need to handle?
thanks in advance
Use db transaction for deleting and this problem will not occur. For example:
Firestore.firestore().runTransaction({(transaction, error) -> Any? in
return transaction.deleteDocument(docReference)
}) { [weak self](object, err) in
// do something when operation is done
}
Both on simulator and my real device, an array of strings is saved upon app termination. When I restart the app and fetchRequest for my persisted data (either from a viewDidLoad or a manual button action), I get an empty array on the first try. It isn't until the second time I fetchRequest that I finally get my data.
The funny thing is that there doesn't seem to be a time discrepancy involved in this issue. I tried setting various timeouts before trying to fetch the second time. It doesn't matter whether I wait 10 seconds to a minute -- or even immediately after the first fetch; the data is only fetched on the second try.
I'm having to use this code to fetch my data:
var results = try self.context.fetch(fetchRequest) as! [NSManagedObject]
while (results.isEmpty) {
results = try self.context.fetch(fetchRequest) as! [NSManagedObject]
}
return results
For my sanity's sake, here's a checklist:
I'm initializing the Core Data Stack using boilerplate code from Apple: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html#//apple_ref/doc/uid/TP40001075-CH4-SW1
I'm putting my single DataController instance in a static variable at the top of my class private static let context: NSManagedObjectContext = DataController().managedObjectContext
I'm successfully saving my context and can retrieve the items without any issue in a single session; but upon trying to fetch on the first try in a subsequent session, I get back an empty array (and there lies the issue).
Note** I forgot to mention that I'm building a framework. I am using CoreData with the framework's bundle identifier and using the model contained in the framework, so I want to avoid having to use logic outside of the framework (other than initalizing the framework in the appDelegate).
The Core Data stack should be initialized in applicationDidFinishLaunchingWithOptions located in appDelegate.swift because the psc is added after you're trying to fetch your data.
That boilerplate code from Apple includes:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
/* ... */
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
The saved data isn't available until the addPersistentStoreWithType call finishes, and that's happening asynchronously on a different queue. It'll finish at some point but your code above is executing before that happens. What you're seeing isn't surprising-- you're basically looping until the async call finishes.
You need to somehow delay your fetch until the persistent store has been loaded. There are a couple of possibilities:
Do something sort of like what you're already doing. I'd prefer to look at the persistent store coordinator's persistentStores property to see if any stores have been loaded rather than repeatedly trying to fetch.
Post a notification after the persistent store is loaded, and do your fetch when the notification happens.
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.