UICollectionViewController reloads images on scroll - ios

I am using the ParseFramework to get some images and display them in a collectionViewController. The thing is, if I scroll before all images are loaded, I experience duplication of images, so I find the same image in different cells. Here’s the code snippets that could be useful for you to be able to help me.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
ExampleCell *cell = (ExampleCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
PFObject * imageObject = [self.imageFilesArray objectAtIndex:indexPath.row];
PFFile * imageFile=[imageObject objectForKey:#"image"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if(!error){
cell.parseImage.image=[UIImage imageWithData:data];
}
}];
return cell;
}
And the method that’s performing the query:
-(void) queryParseMethod{
PFQuery *query = [PFQuery queryWithClassName:#"Allimages"];
[query findObjectsInBackgroundWithBlock:^(NSArray * objects, NSError *error) {
if(!error){
self.imageFilesArray = [[NSArray alloc] initWithArray:objects];
[self.collectionView reloadData];
}
}];
}
This is in ExampleCell.h
#property (weak, nonatomic) IBOutlet UIImageView *parseImage;
This is in ExampleCell.m
#synthesize parseImage;
In the storyboard I am setting the identifier of the cell to imageCell.
I considered the answer below so I modified my method:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ExampleCell *cell = (ExampleCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
if(cell==nil) // no queued cell to dequeue
{
cell = (ExampleCell *)[[UICollectionViewCell alloc] initWithFrame:CGRectMake(0,0,50,50)];
}
// clear any previous image if necessary
cell.parseImage.image = nil;
PFObject * imageObject = [self.imageFilesArray objectAtIndex:indexPath.row];
PFFile * imageFile=[imageObject objectForKey:#"image"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error)
{
if(!error)
cell.parseImage.image=[UIImage imageWithData:data];
}];
return cell;
}
But I still have the same issue. What did I do wrong?

I think you have duplication of images because some cells are reused.
When a cell is no more visible, it is put in a queue where it can be reused for new cell to be displayed. The cell still keep a reference to its image.
So when you dequeue a cell, you should start by clearing its previous image:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ExampleCell *cell = (ExampleCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
if( !cell ) // no queued cell to dequeue
{
// create a new cell to use
}
// clear any previous image if necessary
cell.parseImage.image = nil;
PFObject * imageObject = [self.imageFilesArray objectAtIndex:indexPath.row];
PFFile * imageFile=[imageObject objectForKey:#"image"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error)
{
if(!error)
cell.parseImage.image=[UIImage imageWithData:data];
}];
return cell;
}

So after a lot of investigation I found out the the problem resides in the getDataInBackground..So since getData was working fine for me in terms of not confusing the images with their indexes, I decided to use both getData and getDataInBackground to reach my purpose. I used getDataInBackground to bring the data from PFFile but without displaying it in the cell. When the data is available I will retrieve it using getData.
Here’s the code inside cellForItemAtIndexPath:
//PFCell is the same as ExampleCell previously but of type PFCollectionViewCell
PFCell * cell = (PFCell*) [collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
PFObject * imageObject = [self.imageFilesArray objectAtIndex:indexPath.row];
PFFile * imageFile=[imageObject objectForKey:#"image"];
cell.parseImage.image=nil;
//Getting the data from the PFFile placing it in memory
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error)
{}];
dispatch_async(dispatch_get_main_queue(), ^{
//If the data is now available in memory
if(imageFile.isDataAvailable){
cell.parseImage.image=[UIImage imageWithData:[imageFile getData]];
cell.activityIndicator.hidden=YES;
}
//I added an activity Indicator in case there’s no image yet
else{
[cell.activityIndicator startAnimating];
cell.activityIndicator.hidden=NO;
}
});
return cell;
Now sure there’s a drawback for this. The images will not load unless the user is scrolling so the method will be called again and so the getData.
so I had to add a timer in ViewDidLoad:
[NSTimer scheduledTimerWithTimeInterval:5.0f
target:self selector:#selector(reloadData) userInfo:nil repeats:YES];
And the function:
-(void) reloadData{
NSArray * cellsToReload=[[NSArray alloc] init];
NSMutableArray * temp=[[NSMutableArray alloc] init];
for(int i=0;i<[[self.collectionView visibleCells] count];i++){
PFCell * cell=(PFCell*)[self.collectionView visibleCells][i];
if(cell.parseImage.image==nil){
[temp addObject:[self.collectionView indexPathForCell:cell]];
}
}
cellsToReload=[temp copy];
[self.collectionView reloadItemsAtIndexPaths:cellsToReload];
}
I had to do all this because a simple [self.collectionView reloadData] would flicker the view each 5 seconds.

Related

Download images from parse and show in UICollection View

I am downloading the images from parse and after that I am showing that images in uicollection view but when I scroll the collection view it hangs. Here is my code
-(void)viewWillAppear:(BOOL)animated
{
CGRect screen = [[UIScreen mainScreen] bounds];
CGFloat height = CGRectGetHeight(screen);
PFQuery * query = [PFQuery queryWithClassName:#"Static_Wallpaper"];
[query selectKeys:#[#"thumbnail"]];
if (height == 480) {
[query selectKeys:#[#"image4s"]];
}
else{
[query selectKeys:#[#"image6s"]];
}
[query findObjectsInBackgroundWithBlock:^(NSArray * objects, NSError * error){
if (!error) {
[imgArry arrayByAddingObjectsFromArray:objects];
imgArry = [[NSMutableArray alloc] initWithArray:objects];
NSLog(#"%lu",(unsigned long)imgArry.count);
[cllectionView reloadData];
}
}];
}
and Here is the code populating the images in collection view
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return imgArry.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier = #"Cell";
CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
PFObject *imageObject = [imgArry objectAtIndex:indexPath.row];
PFFile *imageFile = [imageObject objectForKey:#"image4s"];
[imageFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error){
NSLog(#"is there any data? %#", data);
cell.imgView.image = [UIImage imageWithData:data];
;
}
else {
NSLog(#"no data!");
}
}];
return cell;
}
By adding SDWebimage you need to do code like following:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier = #"Cell";
CustomCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
PFObject *imageObject = [imgArry objectAtIndex:indexPath.row];
PFFile *imageFile = [imageObject objectForKey:#"image4s"];
NSString *UserProfile = [file url];
if([[SDWebImageManager sharedManager] diskImageExistsForURL:[NSURL URLWithString:UserProfile]])
{
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:UserProfile]];
UIImage *image = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:key];
cell.imgView.image=image;
}
else
{
cell.imgView.image=[UIImage imageNamed:#"UserUpdationImageCell.png"];
[cell.imgView sd_setImageWithURL:[NSURL URLWithString:UserProfile] placeholderImage:[UIImage imageNamed:#"UserUpdationImageCell.png"]];
}
return cell;
}
Your download task may block the main thread.You can try to use SDWebImage or asynchrous download task and display image

Transfering code from cellForItemAtIndexPath to a CollectionViewCell (Parse Back-End)

I'm using Parse as the database for my app. I want to create a CollectionViewCell and transfer my code there, instead of having it inside the View Controller's cellForItemAtIndexPath. How do I do this?
Thanks.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = #"productCell";
ProductCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
PFObject *product = [self.products objectAtIndex:indexPath.row];
NSString *price = [NSString stringWithFormat:#"$%#.00", product[#"price"]];
cell.price.text = price;
PFFile *userImageFile = product[#"firstThumbnailFile"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *thumbnailImage = [UIImage imageWithData:imageData];
UIImageView *thumbnailImageView = [[UIImageView alloc] initWithImage:thumbnailImage];
cell.image.image = thumbnailImageView.image;
}
}];
return cell;
}
Cell.h
#interface ProductCell : UICollectionViewCell
#property (nonatomic, weak) IBOutlet UIImageView *image;
#property (nonatomic, weak) IBOutlet UILabel *price;
#end
Remember that cellForIndexPath is called over and over as cells scroll into view. So it's bad practice to make unguarded network requests in that method.
If you want to fetch the images lazily, add logic that caches the retrieved results, and only fetch images that haven't been fetched before...
// in private interface
#property(strong,nonatomic) NSMutableDictionary *imageForProduct;
// in init
self.imageForProduct = [#{} mutableCopy];
A method to fetch an image...
- (void)imageForProduct:(PFObject *)product completion:(void (^)(UIImage *))completion {
PFFile *userImageFile = product[#"firstThumbnailFile"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
UIImage *image;
if (!error) {
image = [UIImage imageWithData:imageData];
}
completion(image);
}];
}
Now, in cellForIndexPath, we can't count on the state of the collection being the same by the time the image arrives, so rather than retaining manipulating the cell in the completion block, just reload the index path...
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = #"productCell";
ProductCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
PFObject *product = [self.products objectAtIndex:indexPath.row];
NSString *price = [NSString stringWithFormat:#"$%#.00", product[#"price"]];
cell.price.text = price;
if (self.imageForProduct[product.objectId]) {
cell.image = self.imageForProduct[product.objectId];
} else {
cell.image = // optionally put a placeholder image here
[self imageForProduct:product completion:^(UIImage *)image {
self.imageForProduct[product.objectId] = image;
[collectionView reloadItemsAtIndexPaths:#[indexPath]];
}];
}
return cell;
}
create a method in your custom cell which is exposed in your .h file.
This method should receive an argument of type PFObject.
Then in you cellForItemAtIndexPath, call that method and pass your object in that method.
And in the implementation of that method, extract the details from your object and assign them to respective properties.

Loading images asynchronously, wrong image in cell

I have a UICollectionView in which I am loading multiple images into. From what Ive been reading, in order to match the correct image to each cell I need to subclass UIImageView and get the image there. Because every time I collectionView reloadData, some images duplicate and they are all out of order. But I am unsure how to do this and haven't found any tutorials. I am using Parse for a database.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
albumImageCell *cell = (albumImageCell *) [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[albumImageCell alloc]init];
}
PFObject *temp = [_dataArray objectAtIndex:indexPath.row];
PFFile *file = [temp objectForKey:#"imageThumbnail"];
if (![cell.hasImage isEqualToString:#"YES"]) {
dispatch_async(imageQueue, ^{
NSData *data = [file getData];
if (data) {
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^(void){
cell.imageView.image = image;
cell.hasImage = #"YES";
});
}
});
}
return cell;
}
One way to solve this is to re-query the collection view for the cell again once you're back on the main queue. This code should work:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
albumImageCell *cell = (albumImageCell *) [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[albumImageCell alloc]init];
}
PFObject *temp = [_dataArray objectAtIndex:indexPath.row];
PFFile *file = [temp objectForKey:#"imageThumbnail"];
if (![cell.hasImage isEqualToString:#"YES"]) {
dispatch_async(imageQueue, ^{
NSData *data = [file getData];
if (data) {
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^(void){
// cellAgain will be the actual cell at that index path, if it is visible.
// If it is not visible, cellAgain will be nil.
albumImageCell *cellAgain = [collectionView cellForItemAtIndexPath:indexPath];
cellAgain.imageView.image = image;
cellAgain.hasImage = #"YES";
});
}
});
}
return cell;
}
I made a small 'tutorial' in answer to a this question. Although the question refers to Core Data, my answer applies to any data source so you should be able to fit it around your use case.
One thing you want to watch out for is the inner block, when you get back onto the main queue. Given that you have no idea how long it takes to get to that point, the cell may no longer be relevant to that image (could have been reused), so you need to do a couple of additional checks...
(a) is the image still required?
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath])
(b) is that cell is the correct cell for the image?
UITableViewCell * correctCell = [self.tableView cellForRowAtIndexPath:indexPath];
Although this tutorial is still valid, I tend to abstract things further these days. As the viewController has to deal with thread-unsafe entities like UIKit and Core Data, it is a good idea to keep all viewController code on the main thread. Background queue abstractions should take place at a lower level, preferably in the model code.
What I ended up doing was subclassing UIImaveView and then passing the image file in cellForRow
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
albumImageCell *cell = (albumImageCell *) [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[albumImageCell alloc]init];
}
PFObject *temp = [_dataArray objectAtIndex:indexPath.row];
PFFile *file = [temp objectForKey:#"imageThumbnail"];
[cell.imageView setFile:file];
return cell;
}
And then in the customImageView -
- (void) setFile:(PFFile *)file {
NSString *requestURL = file.url; // Save copy of url locally (will not change in block)
[self setUrl:file.url]; // Save copy of url on the instance
self.image = nil;
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
UIImage *image = [UIImage imageWithData:data];
if ([requestURL isEqualToString:self.url]) {
[self setImage:image];
[self setNeedsDisplay];
}
} else {
NSLog(#"Error on fetching file");
}
}];
}
But this gets Data every time the user scrolls to a new cell. So Im still trying to figure out how to match a particular image to a cell, without getting data every time.

UICollectionView Lag

I have a paging UICollectionView that scrolls through images. Each image fills the screen. For regular photos, my collectionView scrolls fluently but with panoramic shots, it begins to lag as I scroll through the images.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
imageCell *cell = (imageCell *)[collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
cell.tag = indexPath.row;
PFObject *temp = [_dataArray objectAtIndex:indexPath.row];
PFUser *user = [temp objectForKey:#"user"];
PFFile *file = [temp objectForKey:#"image"];
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error){
if (!error) {
cell.selectedImageView.image = [UIImage imageWithData:data];
self.navigationItem.title = [user objectForKey:#"Name"];
}
}];
return cell;
}
As you can see I load the image in the background.
Is it possible I need to do something in willDisplayCell?. Thanks
You are loading data every time. try something to prevent that.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
imageCell *cell = (imageCell *)[collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
...
if (cell.selectedImageView.image == nil)
{
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error){
if (!error) {
cell.selectedImageView.image = [UIImage imageWithData:data];
self.navigationItem.title = [user objectForKey:#"Name"];
}
}];
}
return cell;
}
Also, sometimes it is just in simulator.
Try resetting the simulator or Xcode then run again.
I've experience that sometimes, i even check for possible memory handling error but Restarting simulator did the work.
I never actually used Parse objects before but seems like it's because a cell tries to load multiple images at same time. I think you should move the logic that loads the image to cell and cancel the image loading when it is being reused in prepareForReuse. This idea is commonly used when you load a image in the cell.
I will give you quick example and hopefully it gives you an idea.
in imageCell,
var file: PFFile? {
didSet {
if let f = file {
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error){
if (!error) {
selectedImageView.image = [UIImage imageWithData:data];
}
}];
}
}
}
........
override func prepareForReuse() {
super.prepareForReuse()
if let f = file {
f.cancel()
}
}
in collectionView: cellForItemAtIndexPath:
.....
cell.file = file
.....

In a UICollectionView how can I preload data outside of the cellForItemAtIndexPath to use within it?

Figured out slow loading images were the behind the choppy slow effect of my collectionView.
I've been reading different Q&A's all day and various forum posts. It looks like the best way to solve this issue is to have the data pre-loaded available for the cellForItemAtIndexPath to be able to take what it needs.
I'm not sure how I can do this. I'm using parse as my backend, but sure if given a rough example I'd be able to figure out how to do it. From what I've seen so far I need a separate method to grab the data.
Here is the code:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [[self objects] count];
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
NSArray *people = [self objects];
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
PFObject *current;
current = [people objectAtIndex:indexPath.item];
PFFile *userImageFile = current[#"image"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
UIImage *image = [UIImage imageWithData:imageData];
[[cell contentView] setContentMode: UIViewContentModeScaleAspectFit];
[[cell imageView] setImage:image];
}];
[[cell title] setText:[current valueForKey:#"title"]];
[[cell price] setText:[NSString stringWithFormat: #"£%#", [current valueForKey:#"price"]]];
return cell;
}
So maybe the cellForItemAtIndexPath needs to call that method and take what it needs. Because the data would already be available it won't need to be loaded in the cellForItemAtIndexPath method and the cells will be populated immediately.
Please give suggestions and examples.
I was told a good way to do this would be to check for the image, if non existent provide a placeholder, if it does exist set it. Here are the changes to the above code.
Updates:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
NSArray *people = [self objects];
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
PFObject *current;
current = [people objectAtIndex:indexPath.item];
PFFile *userImageFile = current[#"image"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error)
{
if (!image) {
[[cell imageView] setImage:[UIImage imageNamed:#"placeholder.png"]];
} else {
image = [UIImage imageWithData:imageData];
//resize image
CGSize destinationSize = CGSizeMake(158,187);
UIGraphicsBeginImageContext(destinationSize);
[image drawInRect:CGRectMake(0,0,destinationSize.width, destinationSize.height)];
//New image
UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//Optimise image
NSData *imageDataCompressed = UIImageJPEGRepresentation(newImage, 0.4f);
// NSLog(#"Image Size %#", NSStringFromCGSize(newImage.size));//log size of image
NSLog(#"%#", [current valueForKey:#"title"]);
[[cell imageView] setImage:[UIImage imageWithData:imageDataCompressed]];
}
}
}];
[[cell title] setText:[current valueForKey:#"title"]];
[[cell price] setText:[NSString stringWithFormat: #"£%#", [current valueForKey:#"price"]]];
return cell;
}
Place holder shows fine but remains, how do I know when the image has been loaded so I can make my cells reflect that?
Thanks for your time.
Kind regards.
Update:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
NSArray *people = [self objects];
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
[cell.activityIndicator startAnimating];
PFObject *current;
current = [people objectAtIndex:indexPath.item];
PFFile *userImageFile = current[#"image"];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:userImageFile.url, indexPath.item]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataDontLoad
timeoutInterval:6.0];
[cell.imageView setImageWithURLRequest:urlRequest
placeholderImage:[UIImage imageNamed:#"placeholder.png"]
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
//resize image
CGSize destinationSize = CGSizeMake(158,187);
UIGraphicsBeginImageContext(destinationSize);
[image drawInRect:CGRectMake(0,0,destinationSize.width, destinationSize.height)];
//New image
UIImage*newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//Optimise image
NSData *imageDataCompressed = UIImageJPEGRepresentation(newImage, 0.4f);
cell.imageView.image = [UIImage imageWithData:imageDataCompressed];
NSLog(#"Image Size %#", NSStringFromCGSize(newImage.size));//log size of image
[cell.activityIndicator stopAnimating];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
NSLog(#"Failed to download image: %#", error);
}];
return cell;
}
Latest Update:
Set up a method that gets data from parse.com and stores in an NSMutableDictionary then in a mutable array. I store the title, price and URL to image of the garment.
- (void)grabDataFromCloud
{
self.model = [NSMutableArray array];
for (PFObject *object in [self objects]) {
PFFile *imageFile = [object valueForKey:#"image"];
NSURL *url = [NSURL URLWithString:imageFile.url];
NSMutableDictionary *newObject = [NSMutableDictionary dictionaryWithDictionary:#{#"title": [object valueForKey:#"title"], #"price": [object valueForKey:#"price"], #"imageUrl": url}];
[[self model] addObject:newObject];
}
}
This gets called in my cellForItemsAtIndexPath method.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object
{
[self grabDataFromCloud];
static NSString *CellIdentifier = #"Cell";
VAGGarmentCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: CellIdentifier forIndexPath:indexPath];
[cell.activityIndicator setHidden:YES];
NSMutableDictionary* d = [self.model objectAtIndex:indexPath.item];
cell.title.text = d[#"title"];
cell.price.text = [NSString stringWithFormat:#"£%#", d[#"price"]];
if (d[#"image"]) {
cell.imageView.image = d[#"image"];
} else { // if not, download it
cell.imageView.image = nil;
dispatch_queue_t backgroundQueue = dispatch_queue_create("test", 0);
dispatch_async(backgroundQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:d[#"imageUrl"]];
UIImage* img = [UIImage imageWithData:data];
d[#"image"] = img;
dispatch_async(dispatch_get_main_queue(), ^{
//causes crash Assertion failure in -[UICollectionView _endItemAnimations],
// /SourceCache/UIKit/UIKit-2935.137/UICollectionView.m:3687
// [self.collectionView reloadItemsAtIndexPaths:#[indexPath]];
});
});
}
return cell;
}
I'd suggest you to use AFNetworking's UIImageView+AFNetworking category. It will handle the placeholder etc automatically, and will do everything in a background thread, ensuring that the main thread doesn't get blocked. Specifically, this is the method you'd want to call:
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholderImage;
It is up to you to supply a placeholder image (or nil) when the image is first needed and to start downloading it, and then to hang on to the image once it has been downloaded so that ever after that you can supply it instantly. This example is for a table view, but the principle is exactly the same; the key thing is that my data model is a bunch of NSMutableDictionary objects, and each dictionary in not only the url for the picture we are supposed to have but also a place for keeping the image once it has been downloaded:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
NSMutableDictionary* d = (self.model)[indexPath.row];
cell.textLabel.text = d[#"text"];
if (d[#"im"]) { // if we have a picture, supply it
cell.imageView.image = d[#"im"];
} else if (!d[#"task"]) { // if not, download it
cell.imageView.image = nil;
NSURLSessionTask* task = [self.downloader download:d[#"picurl"]
completionHandler:^(NSURL* url){
if (!url)
return;
NSData* data = [NSData dataWithContentsOfURL:url];
UIImage* im = [UIImage imageWithData:data];
d[#"im"] = im;
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}];
}
return cell;
}
I suggest a different approach.
Google for - or use the search on SO - Asynchrnous loading. Nearly every app programmer faces this issue earlier or later. Consequentially there are tons of tutorials out there.
This is one of them.
http://www.markj.net/iphone-asynchronous-table-image/
I think it is older than the UICollectionView and therfore explains it for UITableView. Both data source delegates are so close to each other that you can easily adopt the solution to your collection.
There are smarter ways of acomplishing your goal. But I think tht this way is a good starting point. You may later want to refactor the solution once you got comforatble with the approach in general.
After several days the issue was my images were far too large. I had to resize them and this instantly solved my issue.
I literally narrowed things down and checked my images to find they were not being resized by the method I thought was resizing them. This is why I need to get myself used to testing.
I learnt a lot about GCD and caching in the past few days but this issue could have been solved much earlier.

Resources