iOS - Fix cellForItemAtIndexPath image loading issue - ios

I am building an app with lots of large image files that are to be displayed in a collection view. Because of the size of the images I've found that it is much quicker to create thumbnails from their URLs, as well as use image caching. When I first implemented this in cellForItemAtIndexPath using GCD I saw a huge reduction in UI lag, but I also noticed that the images in the cells would flicker and change rapidly when the collection view was brought into view and scrolled. I found some other posts about similar issues and they said that checking if the cell is nil first should fix the issue, but unfortunately this seems to create another issue in which many of the images never get loaded. Does anyone know how to fix this?
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
PhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
ObjectWithPhoto *object = self.objects[indexPath.item];
cell.imageView.image = nil;
NSString *imageName = object.imageName;
NSString *imageKey = [NSString stringWithFormat:#"%#_thumbnail", imageName];
if ([[ImageCache sharedCache] imageForKey:imageKey]) {
cell.imageView.image = [[ImageCache sharedCache] imageForKey:imageKey];
} else {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSURL *imageURL = [[NSBundle mainBundle] URLForResource:imageName withExtension:#"jpg"];
CGSize imageSize = CGSizeMake(self.view.frame.size.width, self.view.frame.size.width);
UIImage *thumbnail = [UIImage createThumbnailFromURL:imageURL imageSize:imageSize];
[[ImageCache sharedCache] setImage:thumbnail forKey:imageKey];
dispatch_async(dispatch_get_main_queue(), ^(void) {
PhotoCell *cellToUpdate = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (cellToUpdate) {
cellToUpdate.imageView.image = thumbnail;
} else {
NSLog(#"cell is no long visible");
}
});
});
}
return cell;
}

Maybe you're satisfied with your solution, but I'm not. I think at least one source of weirdness you were seeing is from not clearing the image (or setting it to a placeholder) in the case where the image you need isn't cached. Remember, as soon as you begin scrolling, the images won't be in the cache but the reused cell's images will be set -- and wrongly so, to images for other indexPaths. So, fix one...
if ([[ImageCache sharedCache] imageForKey:imageKey]) {
cell.imageView.image = [[ImageCache sharedCache] imageForKey:imageKey];
} else {
// fix one: clear the cell's image now, if it's set, it's wrong...
cell.imageView.image = nil; // or a placeholder
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
// ...
Second, I throw a yellow flag whenever I see somebody call their own cellForItem datasource method and poke values into the cell. This is more concise and more polite....
cell.imageView.image = nil; // or a placeholder
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSURL *imageURL = [[NSBundle mainBundle] URLForResource:imageName withExtension:#"jpg"];
CGSize imageSize = CGSizeMake(self.view.frame.size.width, self.view.frame.size.width);
UIImage *thumbnail = [UIImage createThumbnailFromURL:imageURL imageSize:imageSize];
[[ImageCache sharedCache] setImage:thumbnail forKey:imageKey];
dispatch_async(dispatch_get_main_queue(), ^(void) {
// fix two: don't get the cell. we know the index path, reload it!
[collectionView reloadItemsAtIndexPaths:#[indexPath]];
// deleted evil stuff that was here
});
});

I think I figured out a fix for this. When checking if the cell was still available or nil, it seemed to return nil even when I swear I could still see it on screen. With this in mind I tried telling the collection view to reload data if the cell came back nil and it works! Smooth scrolling and full of non-flickering images.
PhotoCell *cellToUpdate = (id)[self.collectionView cellForItemAtIndexPath:indexPath];
if (cellToUpdate) {
cellToUpdate.imageView.image = thumbnail;
} else {
NSLog(#"cell is no long visible");
[self.collectionView reloadData];
}

Related

UITableView - cell images changing while scrolling

Hi my problem is that when I scroll TableView the image will appear in a wrong cell, after a few seconds the correct image appears.
here is my code
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
}
[cell setOpaque:NO];
[cell setBackgroundColor: [UIColor clearColor]];
PlaceData *data = [tableData objectAtIndex:indexPath.row];
UILabel *nameLabel = (UILabel *)[cell viewWithTag:100];
UILabel *sciNameLabel = (UILabel *)[cell viewWithTag:200];
UIImageView *thumbnailImageView = (UIImageView *)[cell viewWithTag:300];
nameLabel.text = data.name;
sciNameLabel.text = data.scientific_name;
// get a dispatch queue
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// this will start the image loading in bg
dispatch_async(concurrentQueue, ^{
NSURL *urlToPicture = [NSURL URLWithString:[NSString stringWithFormat:#"%#", data.thumbnail]];
NSData *imgData = [NSData dataWithContentsOfURL:urlToPicture options:0 error:nil];
// This will set the image when loading is finished
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
//dispatch_release(concurrentQueue);
});
});
return cell;
}
please help me
You can try adding following code to your cellForRowAtIndexPath -
1) Assign an index value to your custom cell. For instance,
cell.tag = indexPath.row
2) On main thread, before assigning the image, check if the image belongs the corresponding cell by matching it with the tag.
dispatch_async(dispatch_get_main_queue(), ^{
if(cell.tag == indexPath.row) {
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
}});
});
You are reusing old cells, which is a good thing. However, you are not initializing the image in the cell's image view, which is not such a good thing. What you're describing happens because an old cell, with an image that was already loaded for that cell, is used for the new cell. You are then loading that cell's image in the background (which, again, is good) but it takes a few moments for that image to fully load. In the meantime, the image that was already loaded on the old cell, is displayed (and that's the reason you're seeing a wrong image in that cell, for a few moments).
The solution? add either
thumbnailImageView.image = nil
or
thumbnailImageView.image = someDefaultImageWhileYourRealOneLoads
right before dispatch_queue_t concurrentQueue ....
That way, you won't see the old (irrelevant) image while the real one loads.
I hope this helps. Good luck.
As becauase your ImageView is being loaded in an async dispatch call which is NOT on the main thread and is being called in some other thread so there is a delay in fetching the data from the URL and then converting it to an UIImage. THis process takes a bit of time as you know but you are scrolling the tableview in a faster rate. And as you know cellForRowAtIndexPath reuses any cell that is out of the window so the cell that is being reused might NOT fetched the imagedata that it WAS TO RETRIEVE previously when it was in the Window. Thus it loads the wrong data and then again when async is fired for that specific cell the cell loads that image but there comes the delay.
To overcome this feature as Chronch pointed it out u can leave the imageview as nil OR you can use AFNetworking's own UIImageView catagory which has a superb class to load imageview images quite elegantly
I'll leave u a link to it AFNetworking
I would do all my data binding at - tableView:willDisplayCell:forRowAtIndexPath: only because at cellForRowAtIndexPath your cell hasn't been drawn yet.
Another solution you can use is AFNetworking like someone else mentioned before me.
Swift 3.0
DispatchQueue.main.async(execute: {() -> Void in
if cell.tag == indexPath.row {
var tmpImage = UIImage(data: imgData)
thumbnailImageView.image = tmpImage
}
})
cell.thumbnailimages.image=nil
cell.thumbnailimages.setImageWith(imageurl!)
I think these two lines solve your problem.

table view scrolling smooth with url images

I have a table view that loads title and images from url address. Since I added the images, the scrolling isn't smooth. I have changed the background to clear. The images are low resolution. I prefer accessing them with url, not to downloaded the image and re-upload it on the app. Looking forward for your solutions to make the scrolling smooth. Thanks
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TableCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TableCell" forIndexPath:indexPath];
NSString *str = [[feeds objectAtIndex:indexPath.row] objectForKey: #"title"];
[[cell textLabel] setNumberOfLines:0]; // unlimited number of lines
[[cell textLabel] setFont:[UIFont systemFontOfSize: 16.0]];
cell.backgroundColor = [UIColor clearColor];
cell.contentView.backgroundColor = [UIColor clearColor];
cell.TitleLabel.text=str;
UIImage *pImage=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:feeds2[indexPath.row]]]];;
[cell.ThumbImage setImage:pImage];
return cell;
}
replace your code in cellForRowAtIndexPath:
after this line cell.TitleLabel.text=str;
That way you load each image in the background and as soon as its loaded the corresponding cell is updated on the mainThread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
NSData *imgData = NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:feeds2[indexPath.row]]];
if (imgData) {
UIImage *image = [UIImage imageWithData:imgData];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
UIImage *image = [UIImage imageWithData:imgData];
if (image) {
cell.ThumbImage.image = image;
}
});
});
A better approach is to cache the image , so you dont need to download them each time , the table scroll.
here are some very good references to accomplish this.
LazyTableImages Reference
SDWebImage
UIImageView+AFNetworking
Answer is simply you have to implement the loading with NSOperation where a custom class to handle your download and have your NSOperationQueue as downloadQueue. Every UITableView is a (sub class) UIScrollView therefore you can use the methods directly.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[downloadQueue cancelAllOperations]; // clear your queue
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
// start download only for visible cells
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// start download only for visible cells
}
for more detail information visit this tutorial. There you can really find good solution for your need.

ProgressView not showing in all UICollectionViewCells

My problem is that the ProgressView only shows up in the first 2 cells. What is wrong with my code?
Note: My CollectionView scrolls horizontally and each cell covers the whole screen. I think this might have something to do since I tried showing all cells in the same view and they work fine. All ProgressViews show.
EDITED CODE:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"Cell";
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.02];
PFFile *storeLooks = [self.vestimenta objectForKey:[NSString stringWithFormat:#"image_%ld", (long)indexPath.item]];
NSMutableString *precio = [NSMutableString string];
for (NSString* precios in [self.vestimenta objectForKey:[NSString stringWithFormat:#"precios_%ld", (long)indexPath.item]]) {
[precio appendFormat:#"%#\n", precios];}
[storeLooks getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error && data.length > 0) {
cell.imageFile.image = [UIImage imageWithData:data];
} else {
cell.progressView.hidden = YES;
}
} progressBlock:^(int percentDone) {
float percent = percentDone * 0.02;
[cell.progressView setProgress:percent];
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
}];
return cell;
}
Instead of removing the progress view from the cell, you should simply setHidden:YES.
This way when the cells are reused, the progress view will be present and you can then setHidden:NO when you want to start loading stuff in that cell.
Also be careful with progress blocks inside your cells when they are reused. Remember to either cancel the loading operation if the cell is reused, or make sure the progress block only continues updating it's cell if the cell hasn't been reused for a different data item.
So for example I would set the progress to 0. where you're showing the progress view like so:
VestimentaDetailCell *cell = (VestimentaDetailCell *) [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
cell.progressView.hidden = NO;
[cell.progressView setProgress:0.];
And because you're hiding the progressView when the data is finished loading, I would just get rid of this code in the progressBlock:
if (percentDone == 100){
cell.progressView.hidden = YES;
} else {
cell.progressView.hidden = NO;
}
I think this is related to the cells being reused as they come on screen. Since you are removing the progressView from the cell [cell.progressView removeFromSuperview] , once it is dequeued, it is still missing. You could try overriding the prepareForReuse method in your VestimentaDetailCell class to add it back so that all dequeued cells are brought back to their original state.

iOS - Downloading images asynchronously and using them alongside Core Data

In my Core Data model I have an entity which keeps URLs of images. I want to download these images so I can use them in my UICollectionView model, which is an array of my Core Data entities. I want to always be able to access these images synchronously (assuming they have already been downloaded async) so there's no delay between them loading in their respective cell.
Currently, I am using an async method in the cellForIndexPath: data source delegate method of the UICollectionView but when the collection view is reloaded, if there are images still being downloaded, they get assigned to the wrong cell as cells have been inserted during this time.
This problem seems like it should be very obvious to solve but I cannot work it out.
Any ideas are greatly appreciated.
Thanks.
You can download the images asynchronously in viewDidLoad, and add them to an NSMUtableArray as follows
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < carPhotos.count; i++) {
NSURL *photoURL = [NSURL URLWithString:carPhotos[i]];
NSData *photoData = [NSData dataWithContentsOfURL:photoURL];
UIImage *image = [UIImage imageWithData:photoData];
[carImages addObject:image];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
});
And then check in the cellForRowAtIndexpath to ensure that the indexPath matches the arrayIndex, and if you want load a dummy image for not-loaded images, as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
CarsAppCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.carMakeLabel.text = carMakes[indexPath.row];
cell.carModelLabel.text = carModels[indexPath.row];
if (carImages.count > indexPath.row) {
cell.carImage.image = carImages[indexPath.row];
} else {
cell.carImage.image = [UIImage imageNamed:#"dummy.png"];
}
return cell;
}
I hope it helps....
A couple of things you can do:
In the cell's prepareForReuse you can cancel any (still) pending image request.
In the completion process for the image request you can verify that the image url is still as expected before setting the image.
Something like:
-(void)prepareForReuse
{
[super prepareForReuse];
_imageUrl = nil;
}
-(void)useImageUrl:(NSString*)imageUrl
{
_imageUrl = imageUrl;
if(_imageUrl)
{
__weak typeof(self) weak = self;
[UIImage fetchRemoteImage:imageUrl completion:^(UIImage* image){
if(weak.imageUrl && [weak.imageUrl isEqualToString:imageUrl] && image) {
weak.imageView.image = image;
}
}];
}
}

lazy loading on uicollectionview using dispatch_sync

I want to load too many image after image processing each image.
but when i scroll up and down, lazy loading..
it's my code following.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell;
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"FaceImageCell" forIndexPath:indexPath];
UIImageView *fImageView = (UIImageView*)[cell viewWithTag:1];
[faceImageView setImage:[UIImage imageNamed:#"ic_loading"]];
NSLog(#"Loaded Image row : %d", indexPath.row);
ALAsset *asset;
asset = [_imageList objectAtIndex:indexPath.row];
dispatch_async(all_f_d_queue, ^{
ALAssetRepresentation *representation = [asset defaultRepresentation];
NSString *filename = [representation filename];
NSLog(#"%#", filename);
UIImage *image, *fullImage;
if ((fullImage = [_fullImageCache objectForKey:filename]) == nil) {
image = [UIImage imageWithCGImage:[asset thumbnail]
scale:[representation scale]
orientation:(UIImageOrientation)[representation orientation]];
vector<cv::Rect> f = [ImageUtils findFeature:image minsize:MIN_FACE_SIZE
withCascade:face_cascade];
Mat imageMat = [ImageUtils cvMatFromUIImage:image];
fullImage = [ImageUtils UIImageFromCVMat:imageMat];
[_fullImageCache setObject:fullImage forKey:filename];
}
dispatch_async(dispatch_get_main_queue(), ^{
[fImageView setImage:fullImage];
[cell setNeedsDisplay];
});
});
return cell;
}
Due to the many queue, it occured lazy loading.
I want to stop or cancel dispatch queue when loading cell is invisible.
How should I do for detecting cell to be invisible and for stopping dispatch queue??
Let me know please.
I could use asynctask in android. but I don't know how to implement that in iOS.
you can use NSOperation and NSOperationQueue.
It's pretty much the same as dispatchlib but is Object Oriented.
You can wrap the work on on NSOperation and then add that to an NSOperationQueue.
And then you can cancel an NSOperation whenever you want.
Probably have to read Apple documentations for better explanations of these two classes.
Take a look on this tutorial. It shows how you can download data asynchronously using NSOperationQueue and cancel this queue if user did scroll and this cell is not visible anymore.

Resources