FEventType.ChildAdded event only fired once - ios

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.

Related

How to limit number of requests to a database

I have an app with a settings page where the settings of each user are stored in a MySQL database. I was wondering what is the best way to update the database for every setting the user changes while sending the minimal number of requests as I'm worried that it will crash if it sends too many( it has happened before).
I was thinking about setting a timer for ~5 seconds when the user first changes a setting, and then reset the timer to 5 seconds again if another setting is changed. Once that timer is finished it will send a request to the server to update all the settings at once. It would also constantly store the new values locally to the app, so if the user closes the app before the 5 seconds are up it will send the request once/if the app loads up again.
Is this viable/what's the best way to go about this?
You need to make some logic functions in your app, so i will try make an pseudo codes below. Hope it will give you an idea. I don`t know the MySQL details but i am trying to explain native Swift way.
First of all you should fetch data partly, I mean if you try to fetch all data at the same time your app can work very slow.. That is why we are doing pagination in the app.
As well as pagination you want to refresh the data fresh 5 seconds so i will use Timer object, which will trigger every 5 seconds the API function and catch data based on pagination. Check your below codes and implement step by step to your project. Happy Coding.
var timer: Timer?
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "loadNewDataAutomatically" with the interval of 5 seconds
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.loadNewDataAutomatically), userInfo: nil, repeats: true)
}
#objc func loadNewDataAutomatically(_ pageNumber: Int, _ pageSize: Int, onSuccess: ((Bool) -> Void)?, onError: ((Error) -> Void)?){
// Call your api here
// Send true in onSuccess in case new data exists, sending false will disable pagination
// If page number is first, reset the list
if pageNumber == 1 { self.list = [YourDataModel]() }
// else append the data to list
self.list.append(apiResponseList)
// If Api responds with error
onError?(apiError)
// Else end success with flag true if more data available
let moreDataAvailable = !apiResponseList.isEmpty
onSuccess?(moreDataAvailable)
}
Assuming that the Database (MySQL) is on a server
You can try using WorkManager for this requirement.
When the user changes their settings, save them locally (which you are already doing)
enqueue a Unique Periodic Work Request using WorkManager & set up the time at what interval should the response be sent to the server.
Minimum time interval is around 15 min. but not guaranteed at exactly 15 minutes,
the system fires it when it seems fit, also according to the Constraints that you set on your Work Request.
You can also use a OneTimeWorkRequest, if you don't need periodic, with an Initial Delay of whatever time you need.
Edit: This question was later edited and ios, swift tags were added where previously it was for android.
If anyone comes here searching for something similar for Android, this would work.

iOS: CloudKit perform(query: ) does nothing - closure not executed

I am in process in adding CloudKit to my app to enable iCloud sync. But I ran into problem with my method, that executes query with perform method on private database.
My method worked fine, I then changed a few related methods (just with check if iCloud is available) and suddenly my perform method does nothing. By nothing I mean that nothing in perform(query: ) closure gets executed. I have breakpoint on the first line and others on the next lines but never manage to hit them.
private static func getAppDetailsFromCloud(completion: #escaping (_ appDetails: [CloudAppDetails]?) -> Void) {
var cloudAppDetails = [CloudAppDetails]()
let privateDatabase = CKContainer.default().privateCloudDatabase
let query = CKQuery(recordType: APPID_Type, predicate: NSPredicate(format: "TRUEPREDICATE"))
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
if let error = error {
print(error)
completion(nil)
} else {
if let records = records {
for record in records {
let appId = record.object(forKey: APPID_ID_Property) as? Int
let isDeleted = record.object(forKey: APPID_ISDELETED_Property) as? Int
if let appId = appId, let isDeleted = isDeleted {
cloudAppDetails.append(CloudAppDetails(id: appId, isDeleted: isDeleted == 1))
}
}
completion(cloudAppDetails)
return
}
}
completion(nil)
}
}
My problem starts at privateDatabase.perform line, after that no breakpoints are hit and my execution moves to function which called this one getAppDetailsFromCloud. There is no error...
This is my first time implementing CloudKit and I have no idea why nothing happens in the closure above.
Thanks for help.
EDIT: Forgot to mention that this metod used to work fine and I was able to get records from iCloud. I have not made any edits to it and now it does not work as described :/
EDIT 2: When I run the app without debugger attached then everything works flawlessly. I can sync all data between devices as expected. When I try to debug the code, then I once again get no records from iCloud.
In the completion handler shown here, if there's no error and no results are found, execution will fall through and quietly exit. So, there are two possible conditions happening here: the query isn't running or the query isn't finding any results. I'd perform the following investigative steps, in order:
Check your .entitlements file for the key com.apple.dev.icloud-container-environment. If this key isn't present, then builds from xcode will utilize the development environment. If this key is set, then builds from xcode will access the environment pointed to by this key. (Users that installed this app from Testflight or the app store will always use the production environment).
Open the cloudkit dashboard in the web browser and validate that the records you expect are indeed present in the environment indicated by step 1 and the container you expect. If the records aren't there, then you've found your problem.
If the records appear as expected in the dashboard, then place the breakpoint on the .perform line. If the query is not being called when you expected, then you need to look earlier in the call stack... who was expected to call this function?
If the .perform is being called as expected, then add an else to the if let record statement. Put a breakpoint in the else block. If that fires, then the query ran but found no records.
If, after the above steps, you find that the completion handler absolutely isn't executed, this suggests a malformed query. Try running the query by hand using the cloudkit dashboard and observing the results.
The closure executes asynchronously and usually you need to wait few seconds.
Take into account you can't debug many threads in same way as single. Bcs debugger will not hit breakpoint in closure while you staying in your main thread.
2019, I encountered this issue while working on my CloudKit tasks. Thunk's selected answer didn't help me, so I guess I'm gonna share here my magic. I got the idea of removing the breakpoints and print the results instead. And it worked. But I still need to use breakpoints inside the closure. Well, what I had to do is restart the Xcode. You know the drill in iOS development, if something's not right, restart the Xcode, reconnect the device, and whatnot.

Getting wrong CREATED change from Firestore addSnapshotListener

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
}

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

How to create negative Firebase timestamp in swift

For an iOS app I am working on, I need to fetch messages in descending order i.e the latest message comes first, followed by second newest message etc.
From looking at other SO answers and research it seems that the best approach for my situation is to create a negative timestamp and then persist that to the database as an extra property to messages.
I will then use queryOrderedByChild('negativeTimestamp') to fetch the messages in a observeSingleEvent and then have a childAdded observer to handle messages which are sent once initial calls are made.
In the firebase documentation it says I can get the server timestamp value from this snippet of code firebase.database.ServerValue.TIMESTAMP
How do I write this for Swift 3?
First, see the linked answer in the comments. That answer relies on the client to generate a timestamp that's made negative and written to Firebase.
If you want to have Firebase generate a timestamp, that can be done as well with this little snappy Firebase structure and piece of code.
First, let's take a look at the structure
root
parent_node
-Y8j8a8jsjd0adas
time_stamp: -1492030228007
timestamp: 1492030228007
Next, some code to create and work with that structure:
Define a var we can use within our class that references the Firebase time stamp
let kFirebaseServerValueTimestamp = [".sv":"timestamp"]
and a function that adds an observer to the timestamp node:
func attachObserver() {
let timestampRef = self.ref.child("timestamp")
let parentNodeRef = self.ref.child("parent_node")
var count = 0
timestampRef.observe(.value, with: { snapshot in
if snapshot.exists() {
count += 1
if count > 1 {
let ts = snapshot.value as! Int
let neg_ts = -ts
let childNodeRef = parentNodeRef.childByAutoId()
let childRef = childNodeRef.child("time_stamp")
childRef.setValue(neg_ts)
count = 0
}
}
})
And a function that writes out a timestamp, therefore causing the observer to fire which creates child nodes within the parent_node based on the Firebase time stamp
func doTimestamp() {
let timestampRef = self.ref.child("timestamp")
timestampRef.setValue(kFirebaseServerValueTimestamp)
}
Here's the rundown.
In the attachObserver function, we attach an observer to the timestamp node - that node may or may not exist but if it doesn't it will be created - read on. The code in the closure is called any time an event occurs in the timestamp node.
When the doTimestamp function is called, it creates and writes a timestamp to the timestamp node, which then fires the observer we attached in attachObserver.
The code in the observe closure does the following:
Make sure the snapshot contains something, and if it does, increment a counter (more on that in a bit). If the counter is greater than 1 get the timestamp as an integer from the snapshot. Then, create it's negative and write it back out to Firebase as a child of parent_node.
How this would apply would be anytime you want to timestamp a child node with a Firebase generated timestamp but negative value for reverse loading/sorting - which speaks to the OP question.
The gotcha here is that when this happens
timestampRef.setValue(kFirebaseServerValueTimestamp)
It actually writes twice to the node, which would cause the code in the closer to be called twice.
Maybe a Firebaser can explain that, but we need to ignore the first event and capture the second, which is the actual timestamp.
So the first event will cause the observer closer to fire, making count = 1, which will be ignored due to the if statement.
Then the second event fires, which contains the actual timestamp, and that's what we use to make negative and write out to Firebase.
Hope this helps the OP and the commenters.
Regardless whether it's for Swift or not, another conceptual solution is to rely on Firebase's server time offset value.
It's not as precise as firebase.database.ServerValue.TIMESTAMP, but the difference is usually within milliseconds. The advantage is that it lets you create a negative timestamp on the client without having to update your Firebase node twice.
You grab the server time offset value when you need it from Firebase, generate the negative timestamp on the client, and then save your object in Firebase once.
See:
https://groups.google.com/forum/#!topic/firebase-talk/EXMbZmyGWgE
https://firebase.google.com/docs/database/ios/offline-capabilities#clock-skew (for iOS).
https://firebase.google.com/docs/database/web/offline-capabilities#clock-skew (for web).
var offsetRef = firebase.database().ref(".info/serverTimeOffset");
offsetRef.on("value", function(snap) {
var offset = snap.val();
var negativeTimestamp = (new Date().getTime() + offset) * -1;
});

Resources