cs193p – Assignment #6 Extra Task #4

cs193p – assignment #6 extra task #4

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

Teach yourself how to use NSFileManager and use it along with NSData’s writeToURL:atomically: method to cache image data from Flickr (the photos themselves) into files in your application’s sandbox (you’ll probably want to use the NSCachesDirectory to store them). Maybe you keep the last 20 photos the user looks at or all the photos in Recents or maybe 50MB’s worth of photos? Up to you.

Create a new class with two public class methods. One will cache provided data for a given URL. The other one will return the cached image, if possible.

+ (void)cacheImageData:(NSData *)data forURL:(NSURL *)url;
+ (UIImage *)cachedImageForURL:(NSURL *)url;

If there is no data, there is nothing to do. Otherwise, check if the URL has already been cached and ether update the date of the file, or save the new data:

+ (void)cacheImageData:(NSData *)data forURL:(NSURL *)url
{
    if (!data) return;
    ImageCache *cache = [[ImageCache alloc] initWithURL:url];
    if ([cache.fileManager fileExistsAtPath:[cache.url path]]) {
        [cache.fileManager setAttributes:@{NSFileModificationDate:[NSDate date]}
                            ofItemAtPath:[cache.url path]
                                   error:nil];
    } else {
        [data writeToURL:cache.url atomically:YES];
    }
}

If data has been cached, provide it as image, and update the date of the file:

+ (UIImage *)cachedImageForURL:(NSURL *)url
{
    UIImage *image = nil;
    ImageCache *cache = [[ImageCache alloc] initWithURL:url];
    if ([cache.fileManager fileExistsAtPath:[cache.url path]]) {
        image = [UIImage imageWithData:[NSData dataWithContentsOfURL:cache.url]];
        [cache.fileManager setAttributes:@{NSFileModificationDate:[NSDate date]}
                            ofItemAtPath:[cache.url path]
                                   error:nil];
    }
    return image;
}

Both methods access properties, which get setup in the initializer (basically generating local paths and creating a cache folder if necessary):

@property (nonatomic, strong) NSFileManager *fileManager;
@property (nonatomic, strong) NSURL *folder;
@property (nonatomic, strong) NSURL *url;
...
#define IMAGECACHE_FOLDER @"imageCache"
- (instancetype)initWithURL:(NSURL *)url
{
    self = [super self];
    if (self) {
        _fileManager = [[NSFileManager alloc] init];
        _folder = [[[_fileManager URLsForDirectory:NSCachesDirectory
                                         inDomains:NSUserDomainMask] lastObject]
                   URLByAppendingPathComponent:IMAGECACHE_FOLDER
                   isDirectory:YES];
        BOOL isDir = NO;
        if (![_fileManager fileExistsAtPath:[_folder path]
                               isDirectory:&isDir]) {
            [_fileManager createDirectoryAtURL:_folder
                   withIntermediateDirectories:YES
                                    attributes:nil
                                         error:nil];
        }
        _url = [_folder URLByAppendingPathComponent:[[url path] lastPathComponent]];
    }
    return self;
}

Check if an image has been cached, when the image URL gets set in the image-view controller. If is available, provide it from the cache, otherwise fetch it from Flickr:

- (void)setImageURL:(NSURL *)imageURL
{
    _imageURL = imageURL;    
    UIImage *cachedImage = [ImageCache cachedImageForURL:_imageURL];
    if (cachedImage) {
        self.image = cachedImage;
    } else {
        [self fetchImage];
    }
}

If a new image has been fetched, store it in the cache afterwards:

- (void)fetchImage
{
    ...
        NSData *imageData = [NSData dataWithContentsOfURL:location];
        UIImage *image = [UIImage imageWithData:imageData];
        [ImageCache cacheImageData:imageData forURL:self.imageURL];
        ...
}

Because the image URL is set when the view is not set up yet, move the code to adjust the dimensions of the image, the image and the scroll view to a separate method:

- (void)setImage:(UIImage *)image
{
    self.imageView.image = image;
    [self fitImage:image];
}

- (void)fitImage:(UIImage *)image
{
    self.scrollView.zoomScale = 1.0;
    [self.imageView sizeToFit];
    self.imageView.frame = CGRectMake(0, 0, image.size.width, image.size.height);
    self.scrollView.contentSize = self.image ? self.image.size : CGSizeZero;
    [self.spinner stopAnimating];
    [self setZoomScaleToFillScreen];
}

Additionally, call it when the sub views have been laid out:

- (void)viewDidLayoutSubviews
{
    if (self.imageView.image) [self fitImage:self.imageView.image];    
}

To limit the cache, call a cleanup method after storing new images:

+ (void)cacheImageData:(NSData *)data forURL:(NSURL *)url
{
    ...
    [cache cleanupOldFiles];
}

First get all available files from the cache, with their URLs and dates. Sort them according their date from the oldest to the newest. Enumerate the new sorted array, removing old files as long as the total size exceeds the maximum size. As a second step remove further files till the maximum number of files has been reached:

#define IMAGECACHE_MAXSIZE 1024*1024*50
#define IMAGECACHE_MAXNUMBER 20
- (void)cleanupOldFiles
{
    NSDirectoryEnumerator *dirEnumerator =
    [self.fileManager enumeratorAtURL:self.folder
           includingPropertiesForKeys:@[NSURLAttributeModificationDateKey]
                              options:NSDirectoryEnumerationSkipsHiddenFiles
                         errorHandler:nil];
    NSNumber *fileSize;
    NSDate *fileDate;
    NSMutableArray *files = [NSMutableArray array];
    __block NSUInteger dirSize = 0;
    for (NSURL *url in dirEnumerator) {
        [url getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
        [url getResourceValue:&fileDate forKey:NSURLAttributeModificationDateKey error:nil];
        dirSize += [fileSize integerValue];
        [files addObject:@{@"url":url, @"size":fileSize, @"date":fileDate}];
    }
    NSArray *sorted = [files sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        return [obj1[@"date"] compare:obj2[@"date"]];
    }];
    files = [sorted mutableCopy];
    if (dirSize > IMAGECACHE_MAXSIZE) {
        [sorted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            dirSize -= [obj[@"size"] integerValue];
            NSError *error;
            [self.fileManager removeItemAtURL:obj[@"url"] error:&error];
            [files removeObject:obj];
            *stop = error || (dirSize < IMAGECACHE_MAXSIZE);
        }];
    }
    __block NSUInteger fileCount = [files count];
    if (fileCount > IMAGECACHE_MAXNUMBER) {
        [files enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            fileCount--;
            NSError *error;
            [self.fileManager removeItemAtURL:obj[@"url"] error:&error];
            *stop = error || (fileCount <= IMAGECACHE_MAXNUMBER);
        }];
    }
}

The complete code for the extra task #4 is available on github.

Facebooktwittergoogle_plusredditpinterestlinkedintumblrmailFacebooktwittergoogle_plusredditpinterestlinkedintumblrmail

Flattr this!

3 thoughts on “cs193p – Assignment #6 Extra Task #4”

  1. In InitWithURL, is there a specific reason to choose the lastObject?
    [_fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]

Leave a Reply

Your email address will not be published. Required fields are marked *