How to properly load images on custom UICollectionViewCell? - ios

I'm at this all afternoon and I can't figure it, so your help would be awesome! I have a custom UICollectionViewCell which I'm populating with images I create on the fly. The paths of the images are stored in Parse and after the query is done, I call [self.collectionView reloadData].
In my cellForItemAtIndexPath I have the following code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MovieCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
// Configure the cell
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"original_title" ascending:YES];
[self.moviesforWatchlist sortUsingDescriptors:[NSArray arrayWithObjects:sort, nil]];
cell.userInteractionEnabled = YES;
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[cell addSubview:spinner];
NSDictionary *movie = self.moviesforWatchlist[indexPath.row];
NSString *moviePosterPath = movie[#"poster_path"];
NSString *posterURL = [NSString stringWithFormat:#"http://image.tmdb.org/t/p/w342%#", moviePosterPath];
NSURL *posterImage = [NSURL URLWithString:posterURL];
UIImage *cachedImage = [self.imageCache objectForKey:posterImage];
if (cachedImage) {
cell.imageView.image = cachedImage;
}
else if (cell.imageView.image == nil) {
cell.imageView.image = [UIImage imageNamed:#"default_collectionviewcell.jpeg"];
dispatch_queue_t downloadQueue = dispatch_queue_create("get image data", NULL);
dispatch_async(downloadQueue, ^{
[spinner startAnimating];
NSData *imageData = [NSData dataWithContentsOfURL:posterImage];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
[spinner stopAnimating];
MovieCollectionViewCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = image;
[updateCell reloadInputViews];
[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];
}
});
[self.imageCache setObject:image forKey:posterImage];
});
}
[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];
return cell;
}
I'm calling the query method like this:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self getWatchlistData];
[self.collectionView reloadData];
}
This results in a flashing of images until the images are loaded on to UICollectionViewCells. I want the images displayed in my collectionviewcell to be in alphabetical order, so I'm sorting my array that holds the paths to do so, in cellforitemsatindexpath is this whats causing the flashing of images? Or is it something else? Your help is much appreciated!

There are a number of things I would change:
I wouldn't sort your array in cellForItemsAtIndexPAth. That method is called as each cell is displayed on screen, so you are potentially resorting it numerous times when that is not necessary. If your data is changing between one cell being created and the next, then the re-sorting is likely to mess up the link between the collectionView indexPath.row and your array index. I would sort the array when it is populated, perhaps in your viewWillAppear or getWatchlistData methods. If the data does change after this, use some of the other collectionView delegate/datasource methods to handle inserts/deletes/moves. But I don't think this is causing the flickering of images.
In the code which you post back to the main queue, you call the cellForItemAtIndexPath method, update the cell image and call reloadInputViews (which presumably reconfigures the cell in some way). But you then call reloadItemsAtIndexPaths for all visible cells. What this will do is cause your collectionView to re-call the cellForItemAtIndexPath datasource method for each visible item, causing the image for each to be reloaded from the cache. I think this is the cause of your flickering. I would just save the downloaded image to the cache, and then call reloadItemsAtIndexPaths for just the one indexPath that's affected. The cellForRowAtIndexPath datasource method will be called, and will recover the image from the cache and add it to the new cell.
Likewise, I think the reloadItemsAtIndexPaths at the end of your cellForItemAtIndexPath is superfluous and is likely to contribute to the flickering.
I think you are at risk of some cell reuse problems - for example, you add the spinner to every cell. But the dequeued cells might have been displayed before and therefore already have a spinner (albeit stopped and so probably hidden). If you scroll back and forth a lot, you might end up with cells with loads of spinners, wasting memory. I would give the spinner a tag before adding it as a subView, and then use viewWithTag to determine whether a spinner has already been added to the dequeued cells (if so, remove it). And only add the spinner if necessary (i.e. in your if ...image == nil clause).
Perhaps unlikely, but if the dequeued cell has a default image (so cell.imageView.image != nil) and is being reused for a cell for which the image has not yet been cached (so cachedImage == nil), then none of your dispatch methods will be executed, so the image will never be downloaded.
Sorry if that's a bit heavy, but I hope it's of use.

I have same issue, when any image got download then it flicks the view. I am going before like reload that cell, so it will call some delegate methods of collectionview. So i removed it and make a separate method for reload cell. Now when any new image got reload then i call call this method instead of reloadCellForIndexPath:. In custom method just copy paste code from your cellForItemAtIndexPath:. Assign cell in custom method.
CollectionPhotoItem cell = (CollectionPhotoItem)[self.collection cellForItemAtIndexPath:indexPath];
That's it. it will shows your cell without any flicks. Thanks.

try to use DLImageLoader
[[DLImageLoader sharedInstance] displayImageFromUrl:#"image_url_here"
imageView:#"UIImageView here"];
[[DLImageLoader sharedInstance] loadImageFromUrl:#"image_url_here"
completed:^(NSError *error, UIImage *image) {
if (error == nil) {
// if we have no any errors
} else {
// if we got an error when load an image
}
}];

Related

IOS Objc how to properly load UICollectionViewCell after Scrolling

i'm pretty new in obj/Xcode, and i need to do a UIcollectionView how take images from url request.
I've try to catch the scroll event with scrollViewDidScroll, then call the my request function with the parameter page + 1 or -1; depending of the scroll.
But everytime i'm going on infinite loop, and my cell images donc stop loading, i think its because my way is wrong :
Catching the scroll action :
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
[self reload];
}
The function reload send the request and set the imgs.
How can i load properly the pages, and not 1000times for every scrolling.
Sorry for my english, and thank you
You dont need to call reload like that. As you scroll, 'cellForItemAtIndexPath' is automatically called. If theres a previously used cell in the table (out of view) then the cell is re-used for the new position it is populating - hence 'dequeueReusableCellWithReuseIdentifier' (hope you're using that?!).
So what you need to do is have code on that method itself, that will automatically 'lazy load' the image you want. Heres a copy of a cellForItemOfIndexPath method that achieves just that - the image is populated when its downloaded.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
VideoSearchCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
YoutubeSearchResult *cellSearchResult = [self.ytSearchResults objectAtIndex:indexPath.row];
if (cellSearchResult.thumbnailImage.image == nil)
{
//NSLog(#"loading image");
__weak VideoSearchCell *weakCell = cell;
__weak YoutubeSearchResult *weakResult = cellSearchResult;
NSURL *url = [NSURL URLWithString:cellSearchResult.thumbUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:#"your_placeholder"];
[cellSearchResult.thumbnailImage setImageWithURLRequest:request placeholderImage:placeholderImage success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
weakResult.thumbnailImage.image = image;
weakCell.imageView.image = image;
[weakCell setNeedsLayout];
} failure:nil];
} else {
if (cell.imageView.image != cellSearchResult.thumbnailImage.image)
{
cell.imageView.image = cellSearchResult.thumbnailImage.image;
}
}
return cell;
}
So to explain - VideoSearchCell is my custom cell type. Its fairly simple, it just has a UIImageView on it called 'imageView'. Ive also got an array called 'ytSearchResults' - this contains an array of objects and some contain images, and some dont. As the images download, the results also get populated to that array so I dont download them more than necessary.
The code checks to see if theres an image in that array. If not, it does the downloading code.
By the way, I use __weak references to properties, because of ARC and not wanting to accidentally retain the cell, or the search result. Not enough time to explain all that here though! Hope the code helps.
One more thing - setImageWithURLRequest is a method from the very handy and extremely popular library AFNetworking (2.0). Get hold of that as you will use it all the time.
I do not recommend to load images before cell is actually used, because you never know if user will see that cell. You can start image loading when
- collectionView:cellForItemAtIndexPath: is called, and before starting download request you must check if download of this image is in progress, otherwise you will load it more than once.

How to create custom cell without using reuse Identifier

I am making a program in which I am fetching a image from URL and displaying on the custom cell and rest of the table data on other cells.
Here is the code:
image=[[UIImage alloc]init];
if(indexPath.row==0)
{
LabelTableViewCell *cell=[detailtable dequeueReusableCellWithIdentifier:#"imagecell" forIndexPath:indexPath];
NSOperationQueue *myqueue=[[NSOperationQueue alloc] init];
NSBlockOperation *downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
[cell.actindi startAnimating];
image=[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[frontimage objectAtIndex:0]]]];
}];
downloadOperation.completionBlock=^{
cell.imageprimary.image=image;
[cell.actindi stopAnimating];
};
[myqueue addOperation:downloadOperation];
return cell;
}
else
{
LabelTableViewCell *cell = [detailtable dequeueReusableCellWithIdentifier:#"tabledetail" forIndexPath:indexPath];
cell.labelspec.text=[firstArray objectAtIndex:indexPath.row];
cell.txtfield.text=[secondArray1 objectAtIndex:indexPath.row];
return cell;
}
return 0;
Problem I am facing:
Whenever I scroll up it reloads the top cell.
It dont reload the image on first cell till I click on that cell.
if(!cell) and if (cell==nil) not working.
Reloading the table puts in an infinite loop till I scroll down.
So I want to permanently allocate the memory to the first cell as it is only one in number.
I have tried putting nil in dequeueReusableCellWithIdentifier and its not working.
I have gone about 20-30 articles to do that but nothing has worked for me I don't know why.
So please give me any specific solution at the present condition.
I am new in the iOS developing don't know too much about it . This is my first program that I am trying to make.
You can try it different way:
To download and show image Just use library - https://github.com/rs/SDWebImage
Library will download image and cache for you.

UITableView with asynchronous loading of image in cells - images keep changing on slow connection

I'm loading images asynchronously in my table view. On slow connections, the images in the cell keep flickering/changing (I assume it's from reusing the cells). I've followed the advice on some of the other related questions including:
Caching the image
Setting a placeholder image
It works fine on normal connection, but on EDGE and slow networks, the problem still occurs. I wonder if it is the global queue that is getting backed up with network requests? If so, how do I solve this?
Here is my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// get the data for this photo post
NSDictionary *data = [self.streamPhotosData objectAtIndex:indexPath.row];
static NSString *CellIdentifier = #"StreamPhotoTableViewCell";
__block StreamPhotoTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"StreamPhotoTableViewCell"];
if (cell == nil) {
cell = [[StreamPhotoTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.delegate = self;
// check if image in cache, else cache it
if ([[[GlobalCaches getCurrentInstance] photoCache] objectForKey:[data objectForKey:#"photoGuid"]] != nil) {
// cache hit
cell.photoImageView.image = [[[GlobalCaches getCurrentInstance] photoCache] objectForKey:[data objectForKey:#"photoGuid"]];
} else {
// set a placeholder image
cell.photoImageView.image = [UIImage imageNamed:#"placeholder.png"];
// download the image asynchronously
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[data objectForKey:#"url"]]]];
if (downloadedImage) {
dispatch_async(dispatch_get_main_queue(), ^{
cell.photoImageView.image = downloadedImage;
// update cache
[[[GlobalCaches getCurrentInstance] photoCache] setObject:downloadedImage forKey:[data objectForKey:#"photoGuid"]];
});
}
});
}
return cell;
}
I had a similar issue once.
Why does it happen?
Let's say you're in cellForRow for row number 1, this image is not in the cache, so you're downloading it. Meantime, you scroll the tableView, and let's say that now you're in cellForRow for row number 8, which is the same cell instance.
Let's say that now you have this image in cache, so you're just setting the image with no time offset, and after the cell is with the right image, the image from row number 1 is ready, and remember that it is the same cell instance, thus the image is getting replaced.
Of course this is only one example.
You can try to 'remember' for which row you started to download the image, set a 'beforeTag' before you start the download, and after is, check if it is the same indexPath.row
NSInteger beforeTag = indexPath.row;
// download the image asynchronously
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[data objectForKey:#"url"]]]];
if (downloadedImage) {
dispatch_async(dispatch_get_main_queue(), ^{
if (indexPath.row == beforeTag) {
cell.photoImageView.image = downloadedImage;
}
// update cache
[[[GlobalCaches getCurrentInstance] photoCache] setObject:downloadedImage forKey:[data objectForKey:#"photoGuid"]];
});
}
});
Also, I would put this line in prepareForReuse in your custom cell:
cell.photoImageView.image = [UIImage imageNamed:#"placeholder.png"];
This method is called every time a UITableViewCell is getting reused, it is called just after you 'dequeueReusableCellWithIdentifier', if the cell is already exist. And it's okay to always put a placeHolder image, if the image is in cache, you wont notice this placeholder.
Hope it helps

UITableView lazy-loaded image does not load until finger is off

I have an app where I load a lot of large images. When I lazy-load them, and even after the image has been loaded, the cell does not load them until I take my finger off the screen. I am calling my downloadImageForVisiblePaths function in the UIScrollViewDelegate methods scrollViewDidEndDragging and in scrollViewDidEndDecelerating apart from this, I am also setting the image in the UITableView's cellForRowAtIndexPath method like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// Code to load reusable custom cell
CustomObject *object = (CustomObject*) [self.tableArray objectAtIndex: indexPath];
if(!object.mainImage){
[self downloadImageForIndexPath:indexPath];
cell.mainImageView.image = [UIImage imageNamed:#"placeholder"];
}else{
cell.mainImageView.image = object.mainImage;
}
return cell;
}
Where the downloadImageForIndexPath looks like this:
-(void) downloadImageForIndexPath:(NSIndexPath*) indexPath{
UIImage *loadedImage = [[UIImage alloc] init];
// take url and download image with dispatch_async
// Once the load is done, the following is done
CustomCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.mainImageView.image = loadedImage;
CustomObject *object = (CustomObject*) [self.tableArray objectAtIndex: indexPath];
object.mainImage = loadedImage;
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableVIew reloadData];
});
}
I can't see where I am going wrong. I need the images to load even when the finger is on the screen. This behaviour is similar to how the images load on apps like Google+, Instagram or Facebook.
Any pointers will be much appreciated.
It's hard to tell since you didn't include all the code for downloadImageForIndexPath, but it looks like you are assigning an image to a cell from a background thread (you shouldn't touch UI controls from background threads). Also, if you'r updating cell directly, you don't need to call reloadData.
I would also suggest using SDWebImage for displaying remote images in a tableview.

iOS: Updating tableViewCell infinite loop

This is the problem: I update my tableview after saving and fetching an thumbnail to Core Data and then I tell the cell to update itself - so it can show a thumbnail when the image has been loaded into Core Data. I Use two different threads as Core Data is not threadsafe and all GUI elements of course needs to happen in the main thread.
But this whole method just keep looping forever, and what is causing it is when I reload the thread:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Why? and how do I fix this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Photo"];
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = photo.title;
cell.detailTextLabel.text = photo.subtitle;
NSLog(#"Context %#", self.photographer.managedObjectContext);
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}];
return cell;
}
The infinite loop is caused by calling the cellForRowAtIndexPath: again after the fetch is loaded and the reload on a certain cell is called.
A reload will force the cellForRowAtIndexPath: to be called again... and in your case again and again to infinity.
The solution is simple... Do not reload your cell in the cellForRowAtIndexPath but rather in a callback-method of the fetchrequest. Then reload it there rather then the creation of the cell.
Rather do not load the image inside the cellForRowAtIndexpath: at all.
Whenever your table is instantiated make a method that loops over your datasource and get the respective cell for each item. Then load an image for each cell you deem needed. And reload the cell whenever the fetching of the item is done (for instance a callback method).
If you do want the image to be loaded inside the creation of the cell as you have done now (Although I don't think that is the proper way to do it). You can surround the whole performBlock: with an if-statement checking if the image has already been set or not.
As already been said by others, there should be no need to call reloadRowsAtIndexPaths when the image has been loaded. Assigning a new image to cell.imageView.image is sufficient.
But there is another problem with your code. I assume that self.photographer.managedObjectContext is a managed object context of the "private concurrency type", so that performBlock executes on a background thread. (Otherwise there would be no need to use dispatch_async(dispatch_get_main_queue(), ...).)
So performBlock: executes the code asynchronously, on a background thread. (Which is good because the UI is not blocked.) But when the image has been fetched after some time, the cell might have been reused for a different row. (This happens if you scroll the table view so that the row becomes invisible while the image is fetched.)
Therefore, you have to check if the cell is still at the same position in the tableview:
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
if ([[tableView indexPathForCell:cell] isEqualTo:indexPath]) {
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
}
});
}];
I'd replace the following code:
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
With:
UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
blockCell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[blockCell setNeedsLayout];
This solves the issue of reloading recursively, as well as the possibility that cell has gotten reused in the interim. I'd also check for whether or not you've loaded the photo data already and not update if so.
Since you are using an NSFetchedResultsController though, you may be better off going with Totomus Maximus' answer, if the delegate callbacks are already informing you when the photo data updates. This way just updates the image view and wouldn't update any other information that the Photo update method could change.

Resources