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.
Related
I have a collection view cell with a imageview in it.The data fetched from server is stored in a array.I want the latest image fetched be shown in collection view cell in the beginning instead of at the end.Could anyone help me out with this?
I have correct your code a little, no need to allocate data when your JSON class seems to give it to you. Assuming the serverData is of kind NSMutableArray, this should work. I changed addObject: to insertObject:atIndex:.
-(void) getPhotservice {
[NewJsonHelperClass getExecuteWithParams:msgStr secondParm:nil onCompletion:^(NSDictionary *json){
NSDictionary *user = [json valueForKey:#"user"];
NSMutableArray *dArr = [userDict valueForKey:#"Images"];
for (int i = 0; i <= dArr.count-1; i++) {
reUse = [ReuseVc new];
reUse.photo_link = dArr[i];
// Just add it to the top
[serverData insertObject:reUse atIndex:0];
}
[self.colView reloadData]
}];
}
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.
I am building an app which will query a set of images (thumbnails) from the parse server and then show them in a collection view, similar to how is done on instagram in the users profile page. I created a method which queries the data from the backend successfully :
-(void)queryForTable {
PFQuery *query = [PFQuery queryWithClassName:#"VideoApp"];
NSString * author = [[PFUser currentUser] objectForKey:#"FBName"];
[query whereKey:#"author" equalTo:author];
[query orderByAscending:#"createdAt"];
[query setCachePolicy:kPFCachePolicyNetworkOnly];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
NSLog(#"Successfully retrieved %d objects", objects.count);
[self.collectionView reloadData];
userVideosArray = [[NSMutableArray alloc] initWithCapacity:objects.count];
for (PFObject *object in objects) {
PFFile *thumbnail = [object objectForKey:#"video_thumbnail"];
[thumbnail getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
NSLog(#"Fetching image");
[userVideosArray addObject:[UIImage imageWithData:data]];
} else {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
}
}
}];
}
This method successfully retrieves four objects from the back end, and is called in the ViewDidLoad method.
Then in the collection view cellForRowAtIndexPath method I try to set the queried objects images to the UIImageview on the collectionviewcell as follows:
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
//CollectionViewcellCollectionViewCell * cell = (CollectionViewcellCollectionViewCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 70, 70)];
imageView.backgroundColor = [UIColor grayColor];
[cell addSubview:imageView];
PFQuery *query = [PFQuery queryWithClassName:#"VideoApp"];
NSString * info = [[PFUser currentUser] objectForKey:#"FBName"];
[query whereKey:#"author" equalTo:info];
imageView.image = [UIImage imageWithData:[userVideosArray objectAtIndex:indexPath.row]];
return cell;
}
I keep getting an NSException on imageView.image = [UIImage imageWithData:[userVideosArray objectAtIndex:indexPath.row]]; . Not 100 percent sure why. Any ideas?
You are doing this:
1) reloadData: wrong because you did not update any array before doing this. I assume that it is side effect of findObjectsInBackgroundWithBlock which is definitely wrong, because only object which performs reloadData should be responsible for updating data for datasource.
2) initializing userVideosArray with no items (userVideosArray.count == 0). Looking at your error and knowing that cellForItemAtIndexPath is invoked I'm assuming that -collectionView:numberOfItemsInSection: uses other different array to tell the number of items which is wrong, because you are trying to get item from userVideosArray which may have different number of items
3) filling userVideosArray with items in background. Keeping in mind 1 and 2 gives us an answer to your crash. in cellForItemAtIndexPath you are trying to obtain item which is still not loaded
Btw: [cell addSubview:imageView]; will keep adding image views to your cell each time collection view will reuse it
You should update collectionView after you have handled received data, not before. Your call to [self.collectionView reloadData] trickers calls to cellForItemAtIndexPath and my guess is that userVideosArray does not yet contain as many items as you're expecting.
Anyways, the crash. Try this to prevent crashing:
if (indexPath.item < userVideosArray.count)
imageView.image = [UIImage imageWithData:userVideosArray[indexPath.item]];
Btw when using collectionView, I'd recommend using item instead of row, since one collectionView row might contain several items. You know now what you're doing, and it's ok to use row, but later row vs. item terms might get confusing.
I have a Parse query that runs to gather the 10 closest Arcades in your area, and I am trying to have them display those object titles in 10 separate labels. I have the following code which gathers the 10 closest and logs them, and I am trying to start by displaying the objectId in the labels but cannot figure out how to display them all and not just 1. Any suggestions?
PFQuery *query = [PFQuery queryWithClassName:#"Arcade"];
CLLocation *currentLocation = locationManager.location;
PFGeoPoint *userLocation =
[PFGeoPoint geoPointWithLatitude:currentLocation.coordinate.latitude
longitude:currentLocation.coordinate.longitude];
query.limit = 10;
[query whereKey:kPAWParseLocationKey nearGeoPoint:userLocation withinMiles:kPAWWallPostMaximumSearchDistance];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(#"Successfully retrieved %d scores.", objects.count);
// Do something with the found objects
for (PFObject *object in objects) {
NSLog(#"%#", object.objectId);
NSString *EventTitle = object.objectId;
EventTitle1.text = EventTitle;
for (UIImageView *imageView in self.imageViews) {
__block UIImage *MyPicture = [[UIImage alloc]init];
PFFile *imageFile = [object objectForKey:#"test"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error){
if (!error) {
MyPicture = [UIImage imageWithData:data];
imageView.image = MyPicture;
}
}];
}
for (UILabel *EventLabel in self.EventTitles){
EventLabel.text = object.objectId;
}
}
UPDATE: I have created two collection outlets, however when they display they only display the final object queried, not all 10 of them? Am I doing something wrong?
Your problem is EventTitle1.text = EventTitle;, because you explicitly reference the label. What you should be doing is updating the labels in sequence. This could be done by having the labels in an array (perhaps an IBOutletCollection) and using the iteration index. Or you could tag all of the labels and then look them up (again, using the iteration index).
But, your intended solution isn't simple and doesn't scale. It would be better to use a table view (Parse SDK even gives you an easy way to populate a table view from a query).
I have a IBOutlet Collection view hooked up to 10 labels after pulling objects from a Parse query. My problem is that for some reason it logs 10 different object Ids but only displays one of the object Ids through the collection view. Here is the code I have:PFQuery *query =
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
NSLog(#"Successfully retrieved %d scores.", objects.count);
// Do something with the found objects
int i = 0;
for (PFObject *object in objects) {
NSLog(#"%#", object.objectId);
for (UILabel *EventLabel in self.EventTitles){
(EventLabel *)self.EventTitles[i]= object.objectId;
i++;
}
}
Does anyone see a problem with the code for it to only display one rather than the 10?
The error is that you execute this cycle
for (UILabel *EventLabel in self.EventTitles){
EventLabel.text = object.objectId;
}
within this other cycle
for (PFObject *object in objects) {
}
It means that the first one gets hexecuted every time you get a new object from objects. And each time you get a object from objects you overwrite all the labels with the same objectID. The effect is that at end all the labels will show the objectID of the last object analysed. You should do something like the following instead:
int i = 0;
for (PFObject *object in objects) {
if (i >= [self.EventTitles count]) break;//to make sure we only write up to the max number of UILabels available in EventTitles
(UILabel *) self.EventTitles[i].text = object.objectId;//I assume the "objectId" property of object is an NSString!
i++;
}
You should rename "EventTitles" to "eventTitles" - it's a common rule that Class names start with a capital letter but instance variables ones don't. It will run anyway if you don't change it but it's a good think to do across your code.