Firebase iOS FQuery by child value is failing intermittently - ios

I am using Firebase iOS framework. Don't see an obvious way to tell the build version I am using. I am testing on an iOS simulator usually.
I implemented a helper function to find certain data by indexed key. I have created indexes on these keys in the security rules, but that should not be necessary. My data and query are very similar to the dinosaur query by height example in the firebase docs. My data is flattened to //{key: value, key: value, ...}. E.g., Player/-JwISoamh_jRhYeKvKLk will contain a dictionary like:
{
"height": "1.89",
"firstName": "LeBron",
"lastName": "James"
}
I use my code to find all players with lastName === "James". So, in the code below the ObjBase would point to /Player and childKey = "lastName" and value="James". I expect querySnaphot to contain 1 child with the node for "JwISoamh_jRhYeKvKLk".
Even though the data is in my test data in my app on firebaseio.com and the values match up, this code returns me a querySnapshot.childrenCount == 0:
FQuery *query = [[objBase queryOrderedByChild:childKey] queryEqualToValue:value];
[query observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *querySnapshot) {
Until I change the FEventTypeValue to FEventTypeChildAdded. At that point I get data, but it is not an array of nodes in the querySnapshot.children, but values. As soon as I change the code back to FEventTypeValue my query, exactly as it was before, will work for some period of time (multiple runs of the app). Possibly it stops working when I nuke the data for a bunch of new changes. Maybe it is being cached locally after that initial load.
Perfectly willing to believe I am doing something wrong, but I cannot see what and the fact that it works after I "seed it" with the ChildAdded leads me to think it is an async issue or some initial setup I need to do.
Thanks!!

Related

iOS Firebase - Crash in simulator but not device w/ persistence

I tried googling this problem but it seems like everyone has the opposite problem where the app runs on simulator but not their device. I've been struggling a LOT all week with firebase asynchronous calls returning null and linked the issue to persistence being enabled. All my problems go away if I disable persistence, but I want it enabled. I learned recently about synchronous issues with the different listeners/persistence and have been struggling with firebase returning outdated/nil values for a while.
Simulator was working just a week or two ago and I'm not sure what's changed. I've tried messing with / switching out .observeSingleEvent for .observe and still crashes at this code:
let synced = ref.child("profiles").child((FIRAuth.auth()?.currentUser?.uid)!).child("level")
synced.observeSingleEvent(of: FIRDataEventType.value, with: { (snapshot) in
print(snapshot)
print(snapshot.ref)
if (snapshot.value as! String == "One") {
........//CRASH
With the message:
Could not cast value of type 'NSNull' (0x10b7cf8c8) to 'NSString' (0x10a9dfc40).
When I try to print snapshot, it shows me an empty snapshot. But when I print the ref, the link works and takes me to the right place in my db (where I can see the data exists)
Any ideas how to fix/get around this without disabling persistence? Or more importantly I guess, should I care that it doesn't work in simulator if it works on a device? Will this matter for app store approval / affect future users?
If you'd like to see for yourself that this is an issue of firebase getting a nil/outdated value when the reference exists, here is what I see when I follow the printed ref link
The error seems fairly explicit: there is no value, so you can't convert it to a string.
synced.observeSingleEvent(of: FIRDataEventType.value, with: { (snapshot) in
if (snapshot.exists()) {
if (snapshot.value as! String == "One") {
........//CRASH

FMDB ResultSet always returns only one row

I am trying to use a sqlite database in one of my projects.
It was working fine; but for a reason, something happened and I couldn't find that bug.
The resultSet object always quit from after the first record.
The array always has only 1 record in it. (Probably it left the while because of the error)
I created a DBManager class, and this DBManager class contains different inner Classes. I have a private global FMDatabase instance (And I initialise it somewhere before using it)
As you see, there are 2 different print error line
When I run, the second print line gives this error:
Error calling sqlite3_step (21: out of memory) rs
Error Domain=FMDatabase Code=7 "out of memory" UserInfo=0x790308d0 {NSLocalizedDescription=out of memory}
And the array which should contain over 300 records, has only 1 record in it.
(Last print line is always 1)
This part is looking simple. (I have totally similar code somewhere else, but it works fine)
private var database : FMDatabase!
class DBManager{
class Element{
class func get()->[DataElement]{
database.open()
println( database.lastError() )
var result = [DataElement]()
var resultSet: FMResultSet! = database!.executeQuery("SELECT * FROM Element WHERE working = 1", withArgumentsInArray: nil)
while resultSet.next( ) {
let data = DataElement(
id : Int(resultSet.intForColumn("id")),
name: resultSet.stringForColumn("name"),
server: resultSet.stringForColumn("server"),
working: resultSet.boolForColumn("working") )
result.append( data )
}
println( database.lastError() )
database.close()
println( result.count )
return result
}
}
}
PS: Only difference between those tables is (as I realize ) the "id" column. This Element table has an "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, but the other one does not have any id column. But both of them worked well for a long time.
The "out of memory" error is a misleading SQLite error that means that a SQLite function was called with a NULL value for the sqlite3* pointer. In FMDB this means that you closed the database but then tried to continue using the same FMDatabase instance (without calling open again).
Now, I don't see you doing that in this code sample, but this code sample is employing some practices that make that sort of error possible. Namely, rather than instantiating the FMDatabase object locally, you are using some database property and you run the risk that some other function may have used it (maybe the init method for DataElement? maybe some other function that you removed for the sake of brevity? maybe some other thread?).
Let's imagine that this function called some other function that opened the database again (which FMDB will silently let you do, basically returning immediately if the database is already open), perform some SQL, and then closed the database, then this routine, when it went to retrieve the second row of information, would of have found the database closed, resulting in the error you describe. The opening and closing of the database object is not recursive. Once the subroutine closed it, it's completely closed.
Now all of this is hypothetical, because without seeing what the rest of your code does, it's impossible for me to confirm. But its a scenario that would fit what code you have shared combined with the the symptoms you've described.
Assuming that this is really the case, there are two possible solutions here:
You could simply open the database once and leave it open until the app is terminated. This is probably the easiest approach and is probably more efficient than what you have here. SQLite is actually pretty robust in terms of committing individual SQL statements (or transactions), so people usually leave the database open.
I might even go step further, and suggest that if you might have different threads interacting with this database, that you instantiate a single FMDatabaseQueue object, and use that throughout the app, again not opening and closing all the time. Note, if you use FMDatabaseQueue, you'll want to be even more judicious about making sure that one function that is in the middle of an inDatabase or inTransaction block doesn't call another function that tries to do another inDatabase or inTransaction block (or else you'll deadlock). But like any shared resource, you want to be sensitive to where a resource is locked and when it's released, and databases are no exception to that rule.
If you're determined to open and close the database in every function like this code sample suggests (again, a practice I wouldn't advise), then do not use a class property to keep track of the database. Sure, have some function that opens the database, but return this FMDatabase object, and each function would have it's own local instance and would close that instance when it's done with it.
But you really want to avoid the unintended consequences of one function's closing the database affecting the behavior of some other function.
i also faced the same issue, as after deleting the records from the table i am closing the connection and after some time i am calling one more sql query, at that point of time i got the same issue
"FMDatabase Code=7 "out of memory" UserInfo=0x790308d0 {NSLocalizedDescription=out of memory}"
just add one line of code before closing the connection ie.,
database = nil
database.close()
Later add the method in your modal class as per below
func openDatabase() -> Bool {
if database == nil {
if !FileManager.default.fileExists(atPath: pathToDatabase) {
let documentsDirectory = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString) as String
pathToDatabase = documentsDirectory.appending("/\(databaseFileName)")
print(pathToDatabase)
}
database = FMDatabase(path: pathToDatabase)
}
if !database.isOpen {
database.open()
}
return true
}
Before executing any query check this,
if openDatabase() {
//Query
}
It solved my issue.

Merge two objects of same type

I have two objects:
deviceConfigInfo and deviceStatusInfo
Both contain an array of devices (so theres a third device object actually).
For each device returned in deviceConfigInfo there are these properties:
uuid
name
somethingElse
lookAnotherOne
and for deviceStatusInfo
uuid
name
somethingElse
someStatusInfo
someMoreStuff
(If you hadn't guessed, I just made up some random properties)
So back to that third object I mentioned, device, I created it with all the properties combined. Now, my question is, say the deviceStatusInfo gets updated, how can I update the device object without losing the "old" data that isn't overwritten (in this case, the lookAnotherOne property).
Does it have to be a manual process of getting the device with the matching uuid and then updating each of the properties for deviceStatusInfo or is there a quicker way of doing this? Imagine there were loads of properties.
Hopefully this makes sense. If it helps, I am using Mantle to create the objects/models.
I noticed that Mantle has the following function which I was able to use:
mergeValueForKey:fromModel:
So in my device model, I added two functions:
mergeConfigInfoKeysFromModel:
mergeStatusInfoKeysFromModel:
These functions have access to an array that contains NSString values representing the properties/keys. There is one array for the configInfo and another for statusInfo properties/keys.
I then loop through the keys and use valueForKey to check it has an actual value. If it does, I then call the mergeValueForKey:fromModel:.
Example Code:
- (void)mergeConfigInfoKeysFromModel:(MTLModel *)model
{
NSArray *configInfoKeys = #[#"uuid", #"name", #"somethingElse", #"lookAnotherOne"];
for (NSString *key in configInfoKeys) {
if ([model valueForKey:key]) {
[self mergeValueForKey:key fromModel:model];
}
}
}
All I have to do now, is call the appropriate merge function on the device object when I get an update, passing over the updated device object. Just as below:
[self.device mergeConfigInfoKeysFromModel:deviceUpdate];

How do I get a server timestamp from Firebase's iOS API?

I have an iOS app that uses Firebase and currently has a few dictionaries with keys that are NSDate objects. The obvious issue with this is that NSDate draws from the device's system time, which is not universal.
With that, what's the best way to get a server timestamp (similar to Firebase.ServerValue.TIMESTAMP for the Web API) using Firebase's iOS API so that I can sort my dictionary keys chronologically?
I'm also aware of the chronological nature of IDs generated by childByAutoID, but I can't figure out the proper way to sort these in code. While they may be returned in chronological order, any time something like allKeys is called on them, the order goes out the window.
Any help with this issue would be greatly appreciated!
Update: In Firebase 3.0 + Swift, you can use
FIRServerValue.timestamp(). In Objective-C this is [FIRServerValue timestamp].
In Swift, you can now use FirebaseServerValue.timestamp() with Firebase 2.0.3+ (before 3.0).
The equivalent for Firebase.ServerValue.TIMESTAMP in iOS is kFirebaseServerValueTimestamp. Right now, this only works for Objective-C and not Swift.
In Swift, you can create your own global timestamp with
let kFirebaseServerValueTimestamp = [".sv":"timestamp"]
and then you'll be able to use kFirebaseServerValueTimestamp in the same way.
But you can only use this as the value or priority of a node. You won't be able to set it as the key name (although, I don't believe you could in the Web API either).
In general, calling allKeys on a dictionary does not guarantee order. But if you're using childByAutoID at a node, you can get back the right order by ordering the NSArray returned by allKeys lexicographically. Something like this would work:
[ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
NSDictionary *value = snapshot.value;
NSLog(#"Unsorted allKeys: %#", value.allKeys);
NSArray *sortedAllKeys = [value.allKeys sortedArrayUsingSelector:#selector(compare:)];
NSLog(#"Sorted allKeys: %#", sortedArray);
}];
This is similar to sorting an NSArray alphabetically, but when sorting the auto-generated IDs, you do not want localized or case insensitive sort, so you use compare: instead of localizedCaseInsensitiveCompare:
Caveat: Seems like the timestamp is added AFTER your object is persisted in Firebase. This means that if you have a .Value event listener set up on the location your object is persisted to, it will be triggered TWICE. Once for the initial object being stored in the location, and again for the timestamp being added. Struggled with this issue for days :(
Helpful information for anyone else who can't figure out why their event listeners are triggering twice/multiple times!
As of Firebase 4.0 you can use ServerValue.timestamp()
for example:
let ref = Database.database().reference().child("userExample")
let values = ["fullName": "Joe Bloggs", "timestamp": ServerValue.timestamp()] as [String : Any]
ref.updateChildValues(values) { (err, ref) in
if let err = err {
print("failed to upload user data", err)
return
}
}
You can get Time Stamp using FIRServerValue.timestamp().
But, Because of FIRServerValue.timestamp() listener is called two times. Listener will be called two times.

GAIDictionaryBuilder fails for NSNumber values

I am trying to send commerce transaction data to google analytics on iOS with V3 SDK. I am building the data dictionary using GAIDictionaryBuilder class provided by Google (which is not open source unfortunately). For both createTransactionWithId and createItemWithTransactionId calls, my NSNumber values (revenue, price, etc.) are failing to be added to dictionary data properly. Here is the sample code:
NSMutableDictionary* test = [[GAIDictionaryBuilder createTransactionWithId:(NSString *)transactionId
affiliation:(NSString *)affiliation
revenue:(NSNumber *)revenue
tax:(NSNumber *)tax
shipping:(NSNumber *)shipping
currencyCode:(NSString *)currencyCode] build];
NSLog(#"revenue: %#", revenue);
NSLog(#"TR data: %#", test);
// if I explicitly set the value, IT WORKS!!!!
[test setObject:revenue forKey:#"&tr"];
NSLog(#"TR data FIXED??: %#", test);
In the output, I see revenue correctly, then when logging test dictionary I see the following line corresponding to revenue data:
"&tr" = "<null>";
Then, for the manual fix attempt, I see
"&tr" = "15.25";
as expected.
Here are some clues:
I use the same code in a different project compiled in a different OSX machine without any issues like this.
The transactions are in TRY (Turkish Lira), I suspect Google is trying to fix the separator (',' in Turkish vs '.' everywhere else), but as said above, the other app is also using TRY.
So the question is, why "<null>", why and how does it fail to convert a proper NSNumber to this bizarre value?
Eventually, I fixed the issue by working around it. I assigned the NSNumber to a new one (by getting its floatValue) and it seemed to fix the null values.
By the way, google analytics library version 3.07 readme mentions a similar issue as fixed however neither 3.03 nor 3.07 actually fixed my problem.

Resources