What is the proper way to use NSCache with dispatch_async in a reusable table cell?
Asked Answered
G

2

11

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.

Gorlin answered 11/10, 2013 at 19:59 Comment(3)
You may want to look at the SDWebImage project, which has this all figured out already.Coretta
Can you provide a link?Gorlin
github.com/rs/SDWebImageFortunato
F
15

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:

  1. Use operation queue, which allows you to (a) constrain the number of concurrent requests; and (b) opens up the ability to cancel the operation;

  2. Use a cancelable NSURLSession, too. You can do this yourself or use a library like AFNetworking or SDWebImage.

  3. 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;
}
Fortunato answered 11/10, 2013 at 21:22 Comment(3)
very nice explanation and i also Learn the right thing for that its very nice answer give by to u. actually i just given answer according to question of NSCache. but yes Sdwebimage as well as AFnetworking is also very good for doing this taskDomela
@Fortunato is there a better way to ask the question than how I did that would result in a better understanding of your answer. Thanks for the answer by the way, I found it very useful.Gorlin
@Cedric No, I think your question was fine. You asked "how do I cache", and you provided enough context to keep us from yelling "provide code!" or "what have you tried?!". lol. It's just that the presumption that one should use GCD turns out, in this case, to not be optimal. This is one of those cases where Apple advises using operation queues. And that UIImageView category just takes it a step further, keeping you out of the weeds of that NSOperation-based code. But, if there's something that you don't understand in my answer (esp after watching that video), let us know.Fortunato
D
10

Might be you did some wrong you setting same Key for each image of NSCache

[cache setValue:[UIImage imageWithData:imageData] forKey:@"image"];

Use this Instead of above set ForKey as a Imagepath item.image and use setObject instead of setVlaue:-

[self.imageCache setObject:image forKey:item.image];

try with this Code example:-

in .h Class:-

@property (nonatomic, strong) NSCache *imageCache;

in .m class:-

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.imageCache = [[NSCache alloc] init];

    // the rest of your viewDidLoad
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

     static NSString *cellIdentifier = @"cell";
     NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

     NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
     cell.newsTitle.text = item.title;

    UIImage *cachedImage =   [self.imageCache objectForKey:item.image];;
    if (cachedImage)
    {
        cell.imageView.image = cachedImage;
    }
    else
    {
        cell.imageView.image = [UIImage imageNamed:@"blankthumbnail.png"];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

               NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
               UIImage *image    = nil;
                if (imageData) 
                     image = [UIImage imageWithData:imageData];

                if (image)
                {

                     [self.imageCache setObject:image forKey:item.image];
                }
              dispatch_async(dispatch_get_main_queue(), ^{
                        UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
                        if (updateCell)
                           cell.imageView.image = [UIImage imageWithData:imageData];
                           NSLog(@"Record String = %@",[cache objectForKey:@"image"]);
                  });
          });            
    }
    return cell;
}
Domela answered 11/10, 2013 at 20:19 Comment(6)
WHat happens if the cell is re-used while the image is loading? It seems that you would be setting the wrong image in the cell.Rese
Yes, I was also hoping to cache the objectAtIndex:indexPath.row so that image belong to the respective slot in the cache as to the table row. Right now it will load the wrong picture to the wrong row when you scroll while it is loading. I was hoping that the answers might reflect that since I am using reusable cells. Thanks for your answer by the way.Gorlin
@KendallHelmstetterGelner Because Nitin cleverly is calling cellForRowAtIndexPath inside that final dispatch_async, it ensures that when you reuse the cell for another row of the table, the old dispatched tasks it won't update the wrong row inappropriately. So this implementation gracefully lets the old request finish, and dispatches a new task to the background for the next cell. Quite a good implementation (though on slow connections or large images, a slightly more graceful solution would be to canceling the old requests or somehow deferring them).Fortunato
Good point, I missed the cellForRow call. ALthough I would agree you may want to add canceling of requests at some point.Rese
@Nitin Gohel Thank you for you answer. I was not able to get the changes made to my code (according to your answer) that resulted in satisfactory application change, which is undoubtedly my fault. However, I will be selecting Rob's answer as the better over all solution and answer to my question.Gorlin
@Nitin Gohel. Execellent answer to maintain data in Cache..+100Kuomintang

© 2022 - 2024 — McMap. All rights reserved.