11

I'm seeking a tutorial on how to cache images loaded from a url into cells of a uitableview.

I found an example here

http://www.ericd.net/2009/05/iphone-caching-images-in-memory.html#top

But the code is incomplete. I'm an objective c novice so I found it very difficult to fill in the missing pieces.

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213
dubbeat
  • 7,706
  • 18
  • 70
  • 122
  • Instead of implementing everything yourself, you could use a helper like UIImageLoader (https://github.com/gngrwzrd/UIImageLoader) or SDWebImage. UIImageLoader is only two files and super simple to use. There's a great sample in the repo as well. – gngrwzrd Dec 25 '15 at 20:38

5 Answers5

35

Here is a simple ImageCache implementation using NSCache. ImageCache is a singelton.

ImageCache.h

    #import <Foundation/Foundation.h>

    @interface ImageCache : NSObject

    @property (nonatomic, retain) NSCache *imgCache;


    #pragma mark - Methods

    + (ImageCache*)sharedImageCache;
    //- (void) AddImage:(NSString *)imageURL: (UIImage *)image;
   - (void) AddImage:(NSString *)imageURL withImage:(UIImage *)image;
    - (UIImage*) GetImage:(NSString *)imageURL;
    - (BOOL) DoesExist:(NSString *)imageURL;

    @end

ImageCache.m

  #import "ImageCache.h"

    @implementation ImageCache

    @synthesize imgCache;

    #pragma mark - Methods

    static ImageCache* sharedImageCache = nil;

    +(ImageCache*)sharedImageCache
    {
        @synchronized([ImageCache class])
        {
            if (!sharedImageCache)
                sharedImageCache= [[self alloc] init];

            return sharedImageCache;
        }

        return nil;
    }

    +(id)alloc
    {
        @synchronized([ImageCache class])
        {
            NSAssert(sharedImageCache == nil, @"Attempted to allocate a second instance of a singleton.");
            sharedImageCache = [super alloc];

            return sharedImageCache;
        }

        return nil;
    }

    -(id)init 
    {
        self = [super init];
        if (self != nil) 
        {
            imgCache = [[NSCache alloc] init];
        }

        return self;
    }

   // - (void) AddImage:(NSString *)imageURL: (UIImage *)image
- (void) AddImage:(NSString *)imageURL withImage:(UIImage *)image
    {
        [imgCache setObject:image forKey:imageURL];
    }

    - (NSString*) GetImage:(NSString *)imageURL
    {
        return [imgCache objectForKey:imageURL];
    }

    - (BOOL) DoesExist:(NSString *)imageURL
    {
        if ([imgCache objectForKey:imageURL] == nil)
        {
            return false;
        }

        return true;
    }


    @end

Example

UIImage *image;

    // 1. Check the image cache to see if the image already exists. If so, then use it. If not, then download it.

    if ([[ImageCache sharedImageCache] DoesExist:imgUrl] == true)
    {
        image = [[ImageCache sharedImageCache] GetImage:imgUrl];
    }
    else
    {
        NSData *imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: imgUrl]];
        image = [[UIImage alloc] initWithData:imageData];

        // Add the image to the cache 
        //[[ImageCache sharedImageCache] AddImage:imgUrl :image];

        [[ImageCache sharedImageCache] AddImage:imgUrl withImage:image];
    }
9to5ios
  • 5,319
  • 2
  • 37
  • 65
Flea
  • 11,176
  • 6
  • 72
  • 83
  • in ImageCache.m the method there's a typo: `- (void) AddImage:(NSString *)imageURL: (NSString *)image` should be `- (void) AddImage:(NSString *)imageURL: (UIImage *)image`. The `(NSString *)image` should be `(UIImage *)image`. – Huang Yen-Chieh Jun 21 '12 at 07:13
  • 1
    Thanks for your great answer. It is full fill with req. information – Umut Koseali Jun 13 '13 at 20:16
  • Really helpful answer +1, 1 question where is this cache images are stored i have checked document directory that does not contain it and other question is how could this code will delete this cache images or no need to do that? – Dilip Manek Aug 30 '13 at 12:52
  • 1
    Well done. But I'd suggest 1. following camelCase naming conventions for your methods; and 2. use `dispatch_once` for instantiating your singleton (see [Matt Galloway's singleton](http://www.galloway.me.uk/tutorials/singleton-classes/) discussion. – Rob Sep 04 '13 at 03:44
  • Hi, I do the same as you did here, but it is not working. I want to show the thumbnail image in customCell with NSCache. Here is my post, http://stackoverflow.com/questions/19252991/nsinvalidargumentexception-reason-nscache-setobjectforkeycost-attempt?noredirect=1#comment28501679_19252991 please take have a look on it. Thanks.. – Tulon Oct 08 '13 at 17:25
  • Good one. But once there are a lot of images in the cache, it will cause a memory warning and might eventually crash. Why don't you add an observer for memoryWarning: in init and remove cache objects if the method is fired. – Gautam Jain May 06 '15 at 10:27
  • @GautamJain can you explain a bit more about this? Can you give the code to do that for this example? – MarBVI Apr 17 '16 at 15:37
8

A nice working example was found here

http://ezekiel.vancouver.wsu.edu/~wayne/yellowjacket/YellowJacket.zip

dubbeat
  • 7,706
  • 18
  • 70
  • 122
  • 4
    Updated version for iOS 5: `git clone http://ezekiel.vancouver.wsu.edu/~cs458/demos/YellowJackets/.git` [link](http://ezekiel.vancouver.wsu.edu/~cs458/demos/ImageCache/html/image-cache/) – wcochran Mar 06 '12 at 06:47
2

You could also try using the awesome EgoImage library written by the sharp fellows at enormego to accomplish this. It is very simple to use, makes efficient use of cache behind the scenes and is ideally suited to meet your requirements.

Here's the github path for the library which includes a demo app.

bhavinb
  • 3,278
  • 2
  • 28
  • 26
0

I wrote this (with concepts and some code taken from Lane Roathe's excellent UIImageView+Cache category) for an app I've been working on. It uses the ASIHTTPRequest classes as well, which are great. This could definitely be improved.. for example, by allowing requests to be canceled if no longer needed, or by utilizing the notification userInfo to allow for more precise UI updating.. but it's working well for my purposes.

@implementation ImageFetcher
#define MAX_CACHED_IMAGES 20
static NSMutableDictionary* cache = nil;

+ (void)asyncImageFetch:(UIImage**)anImagePtr withURL:(NSURL*)aUrl {

    if(!cache) {
        cache = [[NSMutableDictionary dictionaryWithCapacity:MAX_CACHED_IMAGES] retain];
    }

    UIImage* newImage = [cache objectForKey:aUrl.description];
    if(!newImage) { // cache miss - doh!
        ASIHTTPRequest *imageRequest = [ASIHTTPRequest requestWithURL:aUrl];    
        imageRequest.userInfo = [NSDictionary dictionaryWithObject:[NSValue valueWithPointer:anImagePtr] forKey:@"imagePtr"];
        imageRequest.delegate = self;
        [imageRequest setDidFinishSelector:@selector(didReceiveImage:)];
        [imageRequest setDidFailSelector:@selector(didNotReceiveImage:)];
        [imageRequest startAsynchronous];
    }
    else { // cache hit - good!
        *anImagePtr = [newImage retain];    
    }
}

+ (void)didReceiveImage:(ASIHTTPRequest *)request {
    NSLog(@"Image data received.");
    UIImage **anImagePtr = [(NSValue*)[request.userInfo objectForKey:@"imagePtr"] pointerValue];

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    UIImage *newImage = [[UIImage imageWithData:[request responseData]] retain];

    if(!newImage) {
        NSLog(@"UIImageView: LoadImage Failed");
    }
    else {
        *anImagePtr = newImage;
        // check to see if we should flush existing cached items before adding this new item
        if( [cache count] >= MAX_CACHED_IMAGES)
            [cache removeAllObjects];

        [cache setValue:newImage forKey:[request url].description];

        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc postNotificationName: @"ImageDidLoad" object: self userInfo:request.userInfo];
    }

    [pool drain];
}

You call this code as follows:

[ImageFetcher asyncImageFetch:&icon withURL:url];

I'm also using notifications, for better or worse, to let any owners of the corresponding UIImage know when they should redisplay- in this case, it's in a tableView context:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(imageDidLoad:) name:@"ImageDidLoad" object:nil];
}

- (void)imageDidLoad:(NSNotification*)notif {
    NSLog(@"Received icon load notification.");
    // reload table view so that new image appears.. would be better if I could
    // only reload the particular UIImageView that holds this image, oh well...
    [self.tableView reloadData];
}

- (void)dealloc {
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc removeObserver:self];
        // ...
}
Steve N
  • 2,667
  • 3
  • 30
  • 37
  • An additional tip here: if you are caching lots of images, or large images, and are therefore using a good amount of memory, you may want to make a "+ (void)flushCache" method that does "[cache removeAllObjects]" so you can call it from within the didReceiveMemoryWarning methods as needed.. that will free up all of that memory. – Steve N Nov 19 '10 at 16:05
0

You also might wanna check HJCache. It comes with a UIImageView compatible view class that does all the caching transparently and is suitable to be used in UITableViewCells where scrolling performance is important.

Ortwin Gentz
  • 52,648
  • 24
  • 135
  • 213