I have been looking for a clear cut way to do this and have not found anywhere that will give an example and explain it very well. I hope you can help me out.
Here is my code that I am using:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"NewsCell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
cell.newsTitle.text = item.title;
NSCache *cache = [_cachedImages objectAtIndex:indexPath.row];
[cache setName:@"image"];
[cache setCountLimit:50];
UIImage *currentImage = [cache objectForKey:@"image"];
if (currentImage) {
NSLog(@"Cached Image Found");
cell.imageView.image = currentImage;
}else {
NSLog(@"No Cached Image");
cell.newsImage.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void)
{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
dispatch_async(dispatch_get_main_queue(), ^(void)
{
cell.newsImage.image = [UIImage imageWithData:imageData];
[cache setValue:[UIImage imageWithData:imageData] forKey:@"image"];
NSLog(@"Record String = %@",[cache objectForKey:@"image"]);
});
});
}
return cell;
}
The cache returns nil for me.
Nitin answered the question about how to use a cache quite well. The thing is, both the original question and Nitin's answer suffer from a problem that you're using GCD, which (a) doesn't provide control over the number of concurrent requests; and (b) the dispatched blocks are not cancelable. Furthermore, you're using dataWithContentsOfURL
, which is not cancelable.
See WWDC 2012 video Asynchronous Design Patterns with Blocks, GCD, and XPC, section 7, "Separate control and data flow", about 48 min into the video for a discussion of why this is problematic, namely if the user quickly scrolls down the list to the 100th item, all of those other 99 requests will be queued up. You can, in extreme cases, use up all of the available worker threads. And iOS only allows five concurrent network requests anyway, so there's no point in using up all of those threads (and if some dispatched blocks start requests that can't start because there are more than five going, some of your network requests will start failing).
So, in addition to your current approach of performing network requests asynchronously and using a cache, you should:
Use operation queue, which allows you to (a) constrain the number of concurrent requests; and (b) opens up the ability to cancel the operation;
Use a cancelable NSURLSession
, too. You can do this yourself or use a library like AFNetworking or SDWebImage.
When a cell is reused, cancel any pending requests (if any) for the previous cell.
This can be done, and we can show you how to do it properly, but it's a lot of code. The best approach is to use one of the many UIImageView
categories, which do cacheing, but also handles all of these other concerns. The UIImageView
category of SDWebImage is pretty good. And it greatly simplifies your code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"NewsCell"; // BTW, stay with your standard single cellIdentifier
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier indexPath:indexPath];
NewsItem *item = newsItemsArray[indexPath.row];
cell.newsTitle.text = item.title;
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:item.image]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
return cell;
}