Assignment #5 Task #3

Please note, this blog entry is from a previous course. You might want to check out the current one.

Cache photo images viewed by the user into files in your application’s sandbox. Each photo’s image should be a separate file in the sandbox. Limit the cache to about 10MB total. When this limit is reached, the oldest photos in the cache should be evicted (deleted) to make room for new photos coming in. Your application should never query Flickr for the image data for a photo that it has in its cache (obviously). This cache should persist between application launches.

For the caching functionality we create a new class providing three public methods:

+ (FlickrCache *)cacheFor:(NSString *)folder;
- (NSURL *)urlForCachedPhoto:(NSDictionary *)photo format:(FlickrPhotoFormat)format;
- (void)cacheData:(NSData *)data ofPhoto:(NSDictionary *)photo format:(FlickrPhotoFormat)format;


The first one provides the cache object and sets up the cache folder. The second one returns the local url of the photo image when it is cached. The third one stores an image in the cache.

The class methods creates a new object and sets the cache folder by appending a given string to the standard cache folder:

+ (FlickrCache *)cacheFor:(NSString *)folder
{
    FlickrCache *cache = [[FlickrCache alloc] init];
    [cache setStandardCacheDirByAppindingString:folder];
    return cache;
}

The standard cache folder is provided by the file manager:

- (void)setStandardCacheDirByAppindingString:(NSString *)string
{
    self.cacheDir = [[[self.fileManager URLsForDirectory:NSCachesDirectory 
                                               inDomains:NSUserDomainMask] lastObject] 
                     URLByAppendingPathComponent:string isDirectory:YES];
}

which we instantiate lazily in its getter

- (NSFileManager *)fileManager
{
    if (!_fileManager)
        _fileManager = [[NSFileManager alloc] init];
    return _fileManager;
}

When setting the new cache folder, we check if it exists, and create it if not:

- (void)setCacheDir:(NSURL *)cacheDir
{
    if (cacheDir == _cacheDir) return;
    _cacheDir = cacheDir;
    BOOL isDir = NO;
    if (![self.fileManager fileExistsAtPath:[_cacheDir path] isDirectory:&isDir] || !isDir)
        [self.fileManager createDirectoryAtURL:_cacheDir 
                   withIntermediateDirectories:YES 
                                    attributes:nil 
                                         error:nil];
}

When looking if photo is cached, we have to create the url for the cached file first. If the file exists we return the url.

- (NSURL *)urlForCachedPhoto:(NSDictionary *)photo format:(FlickrPhotoFormat)format
{
    if (!photo) return nil;
    NSURL *url = [self urlForLocalPhoto:photo format:format];
    if ([self.fileManager fileExistsAtPath:[url path]]) return url;    
    return nil;
}

To create the file name of the cache file, we first create a string out of the photo ID mangle it with the photo format and append this to the cache folder url:

- (NSURL *)urlForLocalPhoto:(NSDictionary *)photo format:(FlickrPhotoFormat)format
{
    if (!photo) return nil;
    NSString *fileName = [NSString stringWithFormat:@"%u-%@", format,
                          [photo objectForKey:FLICKR_PHOTO_ID]];
   return [self.cacheDir URLByAppendingPathComponent:fileName];
}

To store a photo in the cache we simply check if it already exists, otherwise we save its data to our generated local url:

- (void)cacheData:(NSData *)data ofPhoto:(NSDictionary *)photo format:(FlickrPhotoFormat)format
{
    if (!photo) return;
    NSString *path = [[self urlForLocalPhoto:photo format:format] path];
    if ([self.fileManager fileExistsAtPath:path]) return;    
    [self.fileManager createFileAtPath:path contents:data attributes:nil];
    [self cleanupOldFiles];
}

Finally we need to cleanup old files. First we loop over every file inside the cache directory and store its name, size, and create time to an array. If the added up size of all files is higher then allowed, we sort this array in order to have the oldest file first and the newest one last, loop over the sorted array and remove files as long as necessary.

- (void)cleanupOldFiles
{
    NSDirectoryEnumerator *dirEnumerator = [self.fileManager 
                   enumeratorAtURL:self.cacheDir 
        includingPropertiesForKeys:[NSArray arrayWithObject:NSURLCreationDateKey] 
                           options:NSDirectoryEnumerationSkipsHiddenFiles 
                      errorHandler:nil];
    NSString *fileName;
    NSNumber *fileSize;
    NSDate *fileCreation;
    NSMutableArray *files = [NSMutableArray array];
    NSDictionary *fileData;
    __block NSUInteger dirSize = 0;
    for (NSURL *url in dirEnumerator) {
        [url getResourceValue:&fileName forKey:NSURLNameKey error:nil];
        [url getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
        [url getResourceValue:&fileCreation forKey:NSURLCreationDateKey error:nil];
        dirSize += [fileSize integerValue];
        fileData = [[NSDictionary alloc] initWithObjectsAndKeys:url, 
                    @"url", fileSize, @"size", fileCreation, @"date", nil];
        [files addObject:fileData];
    }
    if (dirSize > MAX_FLICKR_CACHE_SIZE) {
        NSArray *sorted = [files sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
            return [[obj1 valueForKey:@"date"] compare:[obj2 valueForKey:@"date"]];
        }];
        [sorted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            dirSize -= [[obj valueForKey:@"size"] integerValue];
            [self.fileManager removeItemAtURL:[obj valueForKey:@"url"] error:nil];
            *stop = dirSize < MAX_FLICKR_CACHE_SIZE;
        }];
    }            
    
}

To use the cache functionality we first have instantiate it with a folder to be used, then have a look if the photo is cached and store it after we have fetched it:

FlickrCache *cache = [FlickrCache cacheFor:@"photos"];
NSURL *url = [cache urlForCachedPhoto:self.photo 
                               format:FlickrPhotoFormatLarge];
if (!url) url = [FlickrFetcher urlForPhoto:self.photo 
                                    format:FlickrPhotoFormatLarge];
NSData *data = [NSData dataWithContentsOfURL:url];

The complete code for this task is available at github.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

Leave a Reply

Your email address will not be published.