I've read that it's important to keep data flatter for Firebase and to also only nest data that you intend to call. I've done those things, but Firebase is still too slow at retrieving data. Here's an example:
My data looks like this:
--English
----Ari : 4
----Philip : 2
----John : 6
And my code looks like this:
[super viewDidLoad];
[[DataSource sharedInstance].selectedLanguageMutableArray removeAllObjects];
//Retrieving Data From Firebase
NSString* selectedLanguagePath = [NSString stringWithFormat:#"languages/%#", [DataSource sharedInstance].languageSelected];
Firebase *languagesRef = [[DataSource sharedInstance].ref childByAppendingPath:selectedLanguagePath];
[[languagesRef queryOrderedByValue] observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
[self.distanceMutableArray addObject:snapshot.key];
NSLog(#"%#", snapshot.key);
NSLog(#"%#", snapshot.value);
NSLog(#"%#", self.distanceMutableArray);
}];
//Selected Languages Mutable Array
[[DataSource sharedInstance].selectedLanguageMutableArray removeAllObjects];
for (NSInteger i = 0; i < self.distanceMutableArray.count; i++) {
UserCustomizationData *item = [[UserCustomizationData alloc] init];
NSString* selectedUser = self.distanceMutableArray[i];
Firebase* selectedUserRef = [[DataSource sharedInstance].usersRef childByAppendingPath:selectedUser];
if (selectedUser.length > 0) {
Firebase* profilePicRef = [selectedUserRef childByAppendingPath:#"profilePicture"];
[profilePicRef observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSString* profPicString = snapshot.value;
NSData *dataFromBase64=[NSData base64DataFromString:profPicString];
UIImage *profPicImage = [[UIImage alloc]initWithData:dataFromBase64];
item.profilePicture = profPicImage;
}];
[[DataSource sharedInstance].selectedLanguageMutableArray addObject:item];
}
}
However, the for loop runs before the self.distanceMutableArray can populate. This throws everything off because the for loop relies on the self.distanceMutableArray being populated.
Is there a way to retrieve data such that the code will run fluidly and in the order that it is written?
The issue here is that Firebase works via asynchronous calls; your code will not work consistently because the code below the Firebase block may be called before the block completes.
You will need to start coding asynchronously and only perform actions on snapshot data after you know for sure it has been populated (inside the block)
[[languagesRef queryOrderedByValue] observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
//at this point, firebase has loaded the snapshot
// so its available to work with
[self.distanceMutableArray addObject:snapshot.key];
for (NSInteger i = 0; i < self.distanceMutableArray.count; i++) {
//do some stuff with the items in snapshot
}
}];
//don't work with anything that was from the snapshot as it may have not been filled yet
However there's an issue as the code is using childAdded, so that will iterate over each item in the firebase node, so that code won't work either as it won't load the array correctly (yes we can fix that by populating the array during each loop).
The additional challenge here is that you need to retrieve data from Firebase based on the result of your first snapshot. Again, same situation exists; only perform actions on that retrieved data after you know for sure it has been retrieved.
One solution is to load in the entire dataset at one time and iterate over it (by value instead of added). If your data sets are smaller that works. However, for big datasets that can be too much.
[[languagesRef queryOrderedByValue] observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//at this point, firebase has loaded the snapshot
// so its available to work with and loaded with
//everything in the node
for ( FDataSnapshot *child in snapshot.children) {
NSDictionary *dict = child.value;
NSString *uid = child.key;
[self.distanceMutableArray addObject:uid];
}
// now the array is loaded do something with it
}];
Another option is to change how your data is stored in firebase so you can retrieve data together so you dont have to make multiple observe calls.
Related
I have recently moved to Realm from Coredata. In my app I am showing 50K + contacts .
The contact object is in the format:
Contact: firstName, lastName ,company
I am trying to fetch all the contacts in the Realm , and I am trying to display those contacts similar to the native contacts app in iPhone.
First I am creating the section header titles based on the contact first name:
-(NSArray *)getSectionTitleBasedOn:(NSString*)sortBy{
RLMResults *results = [self getMainDataSetFromRealm];
ContactSource *contactSource = results.firstObject;
NSMutableDictionary *nameDic = [NSMutableDictionary dictionary];
for (RealmContact *contact in contactSource.contacts){
if (contact.firstName.length>0) {
if ([sortBy isEqualToString:#"FirstName"]) {
[nameDic setObject:#"firstletter" forKey:[contact.firstName substringToIndex:1]];
}
}
}
NSLog(#"dic %#",nameDic);
return [[nameDic allKeys]sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)];
}
This gets me an array of letters which represent the title of section.
Now I am preparing the datasource for each section, so for section A, I am fetching all the contacts that begin with letter 'A'
-(void)prepareDataSource:(NSArray *)titleArr{
RLMResults *results = [self getMainDataSetFromRealm];
ContactSource *contactSource = results.firstObject;
__block NSMutableDictionary *dataSource = [NSMutableDictionary dictionary];
[titleArr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *sectionHeader = obj;
RLMResults *contactResults = [contactSource.contacts objectsWhere:[NSString stringWithFormat:#"firstName BEGINSWITH '%#'",sectionHeader]];
NSMutableArray *contactRowArr = [NSMutableArray array];
for (Contact *contact in contactResults){
[contactRowArr addObject:contact];
}
[dataSource setObject:contactRowArr forKey:sectionHeader];
}];
_dataSource = [dataSource copy];
[self.tableView reloadData];
}
This works really well, but takes 3-5 seconds to load table which is fine but I am looking for ways to improve this data fetch .
Realm works on a principle of lazy-loading, where objects and their properties aren't loaded until you actually 'touch' them for the first time.
As a result, if you do any operations where you're manually iterating through all Realm objects in a results set at once, or manually copying specific objects to an array, you're going to incur a performance hit that will increase the more objects you persist in Realm.
The best way to minimize the performance hit is to try and mitigate how many times you iterate through the results sets and avoid copying objects out of the array as much as possible. RLMResults behaves like an array, so for most scenarios, you can usually just use that object instead.
In the prepareDataSource method, instead of looping through each object and passing them to that NSMutableArray, instead you could consider passing the RLMResults object itself instead.
The method getSectionTitleBasedOn: also seems quite inefficient since you're iterating through every single object in order to check if an entry with a particular first character exists. Instead, you could create an index of the alphabet, and then do a Realm query for entries that start with each letter, and then check to see if the resulting RLMResults object has a positive count (Though I'm not sure if this will actually be any faster).
But in the end, sometimes when you're doing complex sorting like this, where there's no 'clever' way to avoid iterating through each object in a database (Even Realm has to internally load each object when performing a sort), performance hits are unavoidable, in which case you should also make sure your UI has provisions to show a 'working' indicator to the user.
I want to fetch data from server with multiple calls inside for loop. I'm passing different parameter each time. I know it is possible to fetch data like, I'm fetching in code below :
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
[self.tableView reloadData];
});
}
} failure:^(NSError *error) {
}];
}
The problem is, Whenever I add data to dataSourceArry, It is not adding sequentially. It is adding according to response of API calls. Please let me know, If it is not clear.
In your case, I would allocate a mutable array first and set [NSNull null] at each position:
NSInteger count = [[feed objectForKey:#"content"] count];
NSMutableArray *dataSourceArray = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; ++i) {
[dataSourceArray addObject:[NSNull null]];
}
Then, I would use something called dispatch groups (see more here http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/):
__block NSError *apiCallError = nil; // Just to keep track if there was at least one API call error
NSInteger index = 0;
// Create the dispatch group
dispatch_group_t serviceGroup = dispatch_group_create();
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// Start a new service call
dispatch_group_enter(serviceGroup);
// url with feedItem data.
NSURL *url = ...
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// Add data to Data Source
// index should be the correct one, as the completion block will contain a snapshot of the corresponding value of index
dataSourceArray[index] = [placeData objectForKey:#"data"];
}
dispatch_group_leave(serviceGroup);
} failure:^(NSError *error) {
apiCallError = error;
dispatch_group_leave(serviceGroup);
}];
index++;
}
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(),^{
if (apiCallError) {
// Make sure the Data Source contains no [NSNull null] anymore
[dataSourceArray removeObjectIdenticalTo:[NSNull null]];
}
// Reload Table View
[self.tableView reloadData];
});
Hope it works for you.
This might be of help for you,
//keep dictionary property which will store responses
NSMutableDictionary *storeResponses = [[NSMutableDictionary alloc]init];
//Somewhere outside function keep count or for loop
NSInteger count = 0;
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
//Find out index of feddItem
NSInteger indexOfFeedItem = [[feed objectForKey:#"content"] indexOfObject:feedItem];
NSString *keyToStoreResponse = [NSString stringWithFormat:#"%d",indexOfFeedItem];
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
//instead of storing directly to array like below
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
//follow this
//increase count
count++;
[storeResponses setObject:[placeData objectForKey:#"data"] forKey:keyToStoreResponse];
// reloading table view.
if(count == [feed objectForKey:#"content"].count)
{
NSMutableArray *keys = [[storeResponses allKeys] mutableCopy]; //or AllKeys
//sort this array using sort descriptor
//after sorting "keys"
for (NSString *key in keys)
{
//add them serially
[dataSourceArray addObject:[storeResponses objectForKey:key]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
} failure:^(NSError *error) {
}];
}
Edit : The answer I have given is directly written here,you might face compilation errors while actually running this code
Don't reload your table each time in the loop. After the loop finishes fetching data , do a sorting on your datasourcearray to get the desired result and then reload table.
This is because you're calling web-services asynchronously so it's not give guarantee that it's give response in sequence as you have made request!
Now solutions for that :
You should write your api like it's give all data at a time. So,
You not need to make many network call and it will improve
performance also!
Second thing you can make recursive kind of function, I mean make another request from completion handler of previous one. In this case once you got response then only another request will be initialize but in this case you will have to compromise with performance!! So first solution is better according to me!
Another thing you can sort your array after you get all the responses and then you can reload your tableView
Try the following :-
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
dispatch_sync(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
});
} failure:^(NSError *error) {
}];
}
I cant seem to retrieve any information from my firebase database. I created a table with both author and title available.
This is the code i ran in my ios app, but the app just keeps crashing.
// Get a reference to our posts
Firebase *ref = [[Firebase alloc] initWithUrl: #"**********"];
[ref observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSLog(#"%#", snapshot.value[#"author"]);
NSLog(#"%#", snapshot.value[#"title"]);
}];
}
I cant tell whats going wrong, any help with this i will be appreciated.
Thank you
change event type to
[ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
NSLog(#"%#", snapshot.value[#"author"]);
NSLog(#"%#", snapshot.value[#"title"]);
}];
You would generally not store data like that right under your root node - it would generally be a child node(s) stored under the root node, like this:
sizzling-inferno-255
book_node_0
author: test_author
title: test title
book_node_1
author: another author
title: another title title
Firebase *ref = [[Firebase alloc] initWithUrl: #"sizzling-inferno-255"];
[ref observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSLog(#"%#", snapshot.value[#"author"]);
NSLog(#"%#", snapshot.value[#"title"]);
}];
will iterate over each book node and return a snapshot of that node. So the first time a snapshot is returned, it will be
book_node_0
author: test_author
title: test title
and the second time a snapshot is returned will be
book_node_1
author: another author
title: another title title
the book_node keys are generated with childByAutoId.
ChildAdded reads in each child node one at a time as a snapshot.
Value reads in everything in the node; all child nodes, those children etc and can be a large amount of data. If the ref node contains children, like in this answer, all of the nodes are returned and they need to be iterated over to get the child data, so within the observe block...
for child in snapshot.children {
NSLog(#"%#", child.value[#"author"]);
}
Edit - and to specifically answer the OP question, here's how you do it with the Firebase structure in the question:
self.myRootRef = [[Firebase alloc] initWithUrl:#"https://**********"];
[ref observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
NSString *key = snapshot.key;
NSString *value = snapshot.value;
if ( [key isEqualToString:#"author"] ) {
NSLog(#"author = %#", value);
} else if ([snapshot.key isEqualToString:#"title"] ) {
NSLog(#"title = %#", value);
}
}];
I am trying to set up three NSMutableArray to use in UITableView.
Here is my code:
for (PFObject *object in objects) {
PFUser *user = (PFUser *) object[#"user"];
[ [user objectForKey:#"image"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
//Add Comment text
[_commentsArray insertObject:[object objectForKey:#"comment"] atIndex:i];
//Add comment Id
[_commentIDArray insertObject:object.objectId atIndex:i];
//Add user image
[_picsArray insertObject:[UIImage imageWithData:data] atIndex:i ];
if (i == [objects count]-1)
{
[self.tableView reloadData];
}
}
else
{
NSLog(#"Errrror == %ld",(unsigned long)[_picsArray count]);
}
i++;
}];
}
In the PFQuery I am ordering it:
[query orderByDescending:#"createdAt"];
But as far as I can understand image in first row is large. So it takes time to download it. So it goes to second loop. Try to download image. Size is small. Download finished. Add to array. Now download for first image is finished. Add to array but to second place. How can manage it so it add items one by one in the order?
Check this:
// initially, add place holder
for (int i=0; i<objects.count; i++) {
[_commentsArray addObject:#""];
[_commentIDArray addObject:#""];
[_picsArray addObject:#""];
}
for (PFObject *object in objects) {
PFUser *user = (PFUser *) object[#"user"];
[ [user objectForKey:#"image"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
NSInteger orderIndex = [objects indexOfObject:object];
//Replace Comment text
[_commentsArray replaceObjectAtIndex:[object objectForKey:#"comment"] atIndex:orderIndex];
//Replace comment Id
[_commentIDArray replaceObjectAtIndex:object.objectId atIndex:orderIndex];
//Replace user image
[_picsArray replaceObjectAtIndex:[UIImage imageWithData:data] atIndex:orderIndex ];
if (i == [objects count]-1)
{
[self.tableView reloadData];
}
}
else
{
NSLog(#"Errrror == %ld",(unsigned long)[_picsArray count]);
}
i++;
}];
}
Rather than downloading image and create array to populate tableview, you have to just create array of PFObjects and use it with SDWebImage for Asynchronous image downloading without any issue or blocking UI.
I'm guessing that the question is really about not expending effort to download images beyond the scroll position while the visible rows are still being fetched.
The solution to that problem is to load images lazily, when they're needed to configure a cell in cellForRowAtIndexPath:. There's plenty of generic content available about this idea. For a parse-specific solution, see PFImageView here.
The gist is that image view will take care of loading and caching an image from the file. It will do this asynchronously, so there will be a low perceived lag. Just give the file to the image view and let it do the rest...
Your cellForRowAtIndexPath will look something like this:
// just guessing that your "objects" array is the table's datasource
PFObject *object = self.objects[indexPath.row];
PFUser *user = (PFUser *) object[#"user"];
// put a PFImageView in the cell (in this case with a tag==32)
PFImageView *imageView = (PFImageView *)[cell viewWithTag:32];
imageView.image = [UIImage imageNamed:#”placeholder.png”];
imageView.file = [user objectForKey:#"image"]; // assuming this is a file attribute
[imageView loadInBackground];
You have a problem that you try to do order based adding, where your blocks fire asynchronously so it can be in random order.
You should change to a dictionary or any other keyed data structure and use keys for your comments and pics (e.g. use comment id as the key).
Also double check if the callback of the block is executed on the main queue or any serial queue, because if it's not you need to add locks.
I had the same problem, my images were downloaded but not appearing in the order it should, my table view images and the titles were not matching.
To solve that, I created a column at my class in Parse.com that hold exclusively nameForImages, then each downloaded image is saved using this name.
The nameForImages had to be the same used for the column title, for example:
Title = Pepperoni and Four Cheese | nameForImage =
PepperoniAndFourCheese
Title - Muzzarella and Spinach | nameForImage = MuzzarellaAndSpinach
Etc...
This trick fit to solve my problem because the name of the image and the title appearing in the cell were short and had no special caracters.
I hope it helps or light a solution, good luck.
I've been using Parse.com for two months, but this is the first time I get a stupid error which I can't figure out.
I've been working in an app for iOS, but in the beginning I've been working with my own user. Now I registered other users and I want to fetch them from the server.
Well, I add one object in the table Changes, where I have a user property which is a Pointer<User> (it's not _User, but User, it's custom, just an object). Well, when I try to fetch all the rows I have, the one that I have with my user are ok:
so in my debug console is like :
but when I fetch other users:
my debug console is:
so there's not any variable!!!! :(
This is my code:
PFQuery *closestChanges = [PFQuery queryWithClassName:#"Changes"];
[closestChanges whereKey:#"coordinates" nearGeoPoint:geoPoint withinKilometers:0.5f];
[closestChanges findObjectsInBackgroundWithBlock:^(NSArray *changes, NSError *error) {
arrayChanges0 = [[NSMutableArray alloc] init];
arrayChanges1 = [[NSMutableArray alloc] init];
if (changes == nil && changes.count == 0) {
[_tableView reloadData];
return;
}
for (int i = 0; i < changes.count; i++) {
PFObject *currentChange = [changes objectAtIndex:i];
PFObject *user = [currentChange valueForKey:#"user"]; // here my user is null when it's other users.
PFObject *changeToStore = [PFObject objectWithClassName:#"Changes"];
[changeToStore setValue:currentChange[#"changetype"] forKey:#"type"];
[changeToStore setValue:currentChange[#"quantity"] forKey:#"quantity"];
[changeToStore setValue:currentChange[#"date"] forKey:#"enddata"];
[changeToStore setValue:user forKey:#"user"];
if ([currentChange[#"changetype"] intValue] == 0)
[arrayChanges0 addObject:changeToStore];
else
[arrayChanges1 addObject:changeToStore];
}
[_tableView reloadData];
}];
Is there anything wrong I'm doing by adding new users???
Thank you very much in advance.
When you fetch a pointer type from a table, you will only get back the metadata which is objectId. You need to call - (instancetype)includeKey:(NSString *)key in order to get all the data back from Changes table. If you query directly from User table, you will get all the data. But in this case, you User object is a subdocument from Changes objects.
Add this line before performing querying:
[closestChanges includeKey:#"user"]