5

I'm developing an application where I need to download and store images permanently until I manually delete them (in-memory + disk). This is needed because app needs to be able to work offline. I know there are AFNetworking and SDWebImage for caching images but I don't think they allow permanent caching and manual deletion. Afaik, they delete images automatically when cache expires.

Is there any other library for doing this kind of stuff? I tried to write a class for this but It doesn't work very stable. I guess it's better to not reinvent the wheel.

basar
  • 983
  • 9
  • 16
  • There's NSCache and NSPurgeableData but may meet you "permanent" needs. If permanent might not be a cache - more of a store. http://developer.apple.com/library/mac/#technotes/CachingPurgeableMemory/Introduction/Introduction.html – bryanmac Sep 16 '12 at 16:45
  • Why don't you just save them to the documents directory? – Oscar Gomez Sep 17 '12 at 13:29

2 Answers2

0

Save the files in a folder inside Application Support. These will persist and iOS will not delete them. I say use a folder since later, if you do want to delete them, you can just delete the folder.

Exactly how to access and configure the Application Support directory is detailed in this post

If you want, you can save URLs or paths to these images in a Core Data repository.

EDIT: I have a singleton class that manages my Core Data image repository. An object that has an image associated with it has a data property, a URL property, a filePath property, and a boolean flag that says whether there is an outstanding fetch. When something needs an image, it asks for the image (which is constructed from the data), and if there is no data, then the singleton issues a web fetch for it. When it arrives, it gets stored on the file system, and the entity gets a filePath to that image set. It then sends out a notification that such and such an image has arrived. [For debugging, at various points I too tested to insure I could create an image from the data.]

When viewControllers get the notification, they look at the list of visible cells, and if any of them are associated with the newly arrived image, that class asks for an image from the singleton, then sets it on the cell. This is working quite well for me.

Community
  • 1
  • 1
David H
  • 40,852
  • 12
  • 92
  • 138
  • That almost exactly what I'm doing right now. The thing is, downloading images asynchronously, setting them to to table cells etc. is surprisingly painful. Also I'm frequently getting "ImageIO: JPEG Corrupt JPEG data: premature end of data segment" errors altough the JPEG in server is not corrupt. This is why I asked if there was an easy to use library. – basar Sep 17 '12 at 06:11
  • Well, I wouldn't use the word painful, but would say complex. When you receive the data now, the last bit, and you get the connectionDidFinishLoading, then (as a debugging technique) try and see if you can create an image 'UIImage *img = [UIImage alloc] initWithData:data]' - if you get frequent failurs then your data handling has a problem - if it never fails but you get failures later then its your storage/retrieval that's failing. – David H Sep 17 '12 at 13:22
0

Unfortunately SDWebImage Does not provide such a capability so to make use of the advanced caching capabilities provided by SDWebImage i wrote a wrapper around SDWebImage

basically this class manages a fallback permanent cache so if the requested image not found in the SDWeb image cache "disk and memory" it will look for it in the permanent cache

using this approach i managed to keep setting images to table cells as smooth as usual with SDWebImage

.h file

@interface CacheManager : NSObject
+ (CacheManager*)sharedManager;

// Images
- (BOOL) diskImageExistsForURL:(NSURL*) url;
- (void) downloadImage:(NSURL*) url completed:(void(^)(UIImage*))onComplete;
- (void) setImage:(NSURL*)url toImageView:(UIImageView*)iv completed:(void(^)(UIImage*))onComplete;
- (void) clearImageCache;

@end

.m file

#import "CacheManager.h"
#import <SDWebImage/UIImageView+WebCache.h>


#define CACH_IMAGES_FOLDER      @"ImagesData"


@implementation CacheManager


static CacheManager *sharedManager = nil;

#pragma mark -
#pragma mark Singilton Init Methods
// init shared Cache singleton.
+ (CacheManager*)sharedManager{
    @synchronized(self){
        if ( !sharedManager ){
            sharedManager = [[CacheManager alloc] init];
        }
    }
    return sharedManager;
}

// Dealloc shared API singleton.
+ (id)alloc{
    @synchronized( self ){
        NSAssert(sharedManager == nil, @"Attempted to allocate a second instance of a singleton.");
        return [super alloc];
    }
    return nil;
}

// Init the manager
- (id)init{
    if ( self = [super init] ){}
    return self;
}

/**
  @returns YES if image found in the permanent cache or the cache managed by SDWebImage lib
 */
- (BOOL) diskImageExistsForURL:(NSURL*) url{

    // look for image in the SDWebImage cache
    SDWebImageManager *manager = [SDWebImageManager sharedManager];
    if([manager diskImageExistsForURL:url])
        return YES;

    // look for image in the permanent cache
    NSString *stringPath = url.path;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    return [fileManager fileExistsAtPath:stringPath];
}

/**
 get the image with specified remote url asynchronosly 
 first looks for the image in SDWeb cache to make use of the disk and memory cache provided by SDWebImage
 if not found, looks for it in the permanent cache we are managing, finally if not found in either places
 it will download it using SDWebImage and cache it.
 */
- (void) downloadImage:(NSURL*) url completed:(void(^)(UIImage*))onComplete{

    NSString *localPath = [[self getLocalUrlForImageUrl:url] path];
    NSFileManager *fileManager = [NSFileManager defaultManager];

    // -1 look for image in SDWeb cache
    SDWebImageManager *manager = [SDWebImageManager sharedManager];
    if([manager diskImageExistsForURL:url]){
        [manager downloadImageWithURL:url options:SDWebImageRetryFailed
                             progress:^(NSInteger receivedSize, NSInteger expectedSize) {}
                            completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                                onComplete(image);

                                // save the image to the perminant cache for later
                                // if not saved before
                                if(image){
                                    if ([fileManager fileExistsAtPath:localPath]){
                                        NSURL* localeUrl = [self getLocalUrlForImageUrl:url];
                                        [self saveImage:image toCacheWithLocalPath:localeUrl];
                                    }
                                }
                            }];
        return;
    }

    // -2 look for the image in the permanent cache
    if ([fileManager fileExistsAtPath:localPath]){
        UIImage *img = [self getImageFromCache:url];
        onComplete(img);
        // save image back to the SDWeb image cache to make use of the memory cache
        // provided by SDWebImage in later requests
        [manager saveImageToCache:img forURL:url];
        return;
    }

    // -3 download the image using SDWebImage lib
    [manager downloadImageWithURL:url options:SDWebImageRetryFailed
                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {}
                        completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                            onComplete(image);
                            // save the image to the permanent cache for later
                            if(image){
                                NSURL* localeUrl = [self getLocalUrlForImageUrl:url];
                                [self saveImage:image toCacheWithLocalPath:localeUrl];
                            }
                        }];

}

- (void) setImage:(NSURL*)url toImageView:(UIImageView*)iv completed:(void(^)(UIImage*))onComplete{
    [self downloadImage:url completed:^(UIImage * downloadedImage) {
        iv.image = downloadedImage;
        onComplete(downloadedImage);
    }];
}


/**
 @param:imgUrl : local url of image to read
 */
- (UIImage*) getImageFromCache:(NSURL*)imgUrl{
    return [UIImage imageWithData: [NSData dataWithContentsOfURL:imgUrl]];
}

/**
 writes the suplied image to the local path provided
 */
-(void) saveImage:(UIImage*)img toCacheWithLocalPath:(NSURL*)localPath{
    NSData * binaryImageData = UIImagePNGRepresentation(img);
    [binaryImageData writeToFile:[localPath path] atomically:YES];
}

// Generate local image URL baesd on the name of the remote image
// this assumes the remote images already has unique names
- (NSURL*)getLocalUrlForImageUrl:(NSURL*)imgUrl{
    // Saving an offline copy of the data.
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *cachesDirectory = [paths objectAtIndex:0];
    NSString *folderPath = [cachesDirectory stringByAppendingPathComponent:CACH_IMAGES_FOLDER];
    BOOL isDir;
    // create folder not exist
    if (![fileManager fileExistsAtPath:folderPath isDirectory:&isDir]){
        NSError *dirWriteError = nil;
        if (![fileManager createDirectoryAtPath:folderPath withIntermediateDirectories:YES attributes:nil error:&dirWriteError]){
            NSLog(@"Error: failed to create folder!");
        }
    }

    NSString *imgName = [[[imgUrl path] lastPathComponent] stringByDeletingPathExtension];

    NSURL *cachesDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    NSString *pathString = [NSString stringWithFormat:@"%@/%@", CACH_IMAGES_FOLDER, imgName];
    return [cachesDirectoryURL URLByAppendingPathComponent:pathString];
}


/**
 removes the folder contating the cahced images,
 the folder will be reacreated whenever a new image is being saved to the permanent cache
 */
-(void)clearImageCache{
    // set the directory path
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *cachesDirectory = [paths objectAtIndex:0];
    NSString *folderPath =  [cachesDirectory stringByAppendingPathComponent:CACH_IMAGES_FOLDER];
    BOOL isDir;
    NSError *dirError = nil;
    // folder exist
    if ([fileManager fileExistsAtPath:folderPath isDirectory:&isDir]){
        if (![fileManager removeItemAtPath:folderPath error:&dirError])
        NSLog(@"Failed to remove folder");
    }
}

@end
MolhamStein
  • 617
  • 8
  • 14