cs193p – Assignment #5 Task #4

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

Cache photo image data 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’s size to something testable. When this limit is reached, the least recently viewed photos in the cache should be evicted (deleted) to make room for new photos coming in (remember that you can look at a file URL’s resource values to find out when it was last accessed). 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.

Before loading an image, check if it is already in the file cache, and load it from there if possible. After the image has been loaded save it to the cache:

- (void)resetImage
{
        ...
            NSData *imageData;
            NSURL *cachedURL = [FlickrCache cachedURLforURL:imageURL];
            if (cachedURL) {
                imageData = [[NSData alloc] initWithContentsOfURL:cachedURL];                
            } else {                
                [NetworkActivityIndicator start];
                //[NSThread sleepForTimeInterval:2.0];
                imageData = [[NSData alloc] initWithContentsOfURL:self.imageURL];
                [NetworkActivityIndicator stop];
            }
            [FlickrCache cacheData:imageData forURL:self.imageURL];
            ...
}

For the handling of the cache create a new class derived from NSObject with two class methods to get an URL for a stored file (if there is any), or save data to the cache. The cache will a use a folder of its own, and will be limited to different sizes depending on the device:

#define FLICKRCACHE_MAXSIZE_IPHONE 1024*1024*3
#define FLICKRCACHE_MAXSIZE_IPAD 1024*1024*10
#define FLICKRCACHE_FOLDER @"flickrPhotos"
...
+ (NSURL *)cachedURLforURL:(NSURL *)url;
+ (void)cacheData:(NSData *)data forURL:(NSURL *)url;

The first class method creates a cache object gets a possible local URL, which it returns if the file exists:

+ (NSURL *)cachedURLforURL:(NSURL *)url
{
    FlickrCache *cache = [[FlickrCache alloc] init];
    NSURL *cachedUrl = [cache getCachedURLforURL:url];
    if ([cache fileExistsAtURL:cachedUrl]) {
        return cachedUrl;
    }
    return nil;
}

To create a local URL the file name of the original URL is separated from the rest and added to the URL to the cache Folder:

- (NSURL *)getCachedURLforURL:(NSURL *)url
{
    return [self.cacheFolder URLByAppendingPathComponent:
            [[url path] lastPathComponent]];    
}

Because the URL to the cache folder is used in various places, create a property and initialize it lazily. The cache folder should be situated in the cache directory of the application and will have a specific name provided as constant in the header file above. Furthermore check if this folder already exists and create it if not:

@property (nonatomic, strong) NSURL *cacheFolder;
...
- (NSURL *)cacheFolder
{
    if (!_cacheFolder) {
        _cacheFolder = [[[self.fileManager URLsForDirectory:NSCachesDirectory
                                                  inDomains:NSUserDomainMask] lastObject]
                        URLByAppendingPathComponent:FLICKRCACHE_FOLDER
                                        isDirectory:YES];
        BOOL isDir = NO;
        if (![self.fileManager fileExistsAtPath:[_cacheFolder path]
                                    isDirectory:&isDir]) {
            [self.fileManager createDirectoryAtURL:_cacheFolder
                       withIntermediateDirectories:YES
                                        attributes:nil
                                             error:nil];
        }
    }
    return _cacheFolder;
}

To reuse the file manager create a property and initialize it lazily:

@property (nonatomic, strong) NSFileManager *fileManager;
...
- (NSFileManager *)fileManager
{
    if (!_fileManager) {
        _fileManager = [[NSFileManager alloc] init];
    }
    return _fileManager;
}

Checking if an URL exists just means to access the file manager and use a suitable path representation of the URL:

- (BOOL)fileExistsAtURL:(NSURL *)url
{
    return [self.fileManager fileExistsAtPath:[url path]];
}

To save data to the cache, first check if there is actual data, because it is useless to cache an empty file. Like above create a cache object and generate an URL pointing to the local cache file. If the file does already exist, just updated its modification date. Otherwise create the file. … and finally cleanup old files:

+ (void)cacheData:(NSData *)data forURL:(NSURL *)url
{
    if (!data) return;
    FlickrCache *cache = [[FlickrCache alloc] init];
    NSURL *cachedUrl = [cache getCachedURLforURL:url];
    if ([cache fileExistsAtURL:cachedUrl]) {
        [cache.fileManager setAttributes:@{NSFileModificationDate:[NSDate date]}
                            ofItemAtPath:[cachedUrl path] error:nil];
    } else {
        [cache.fileManager createFileAtPath:[cachedUrl path]
                                   contents:data attributes:nil];
        [cache cleanupOldFiles];
    }
}

For the cleanup it is necessary to get a list of files in the cache which can be done by enumerating over all files (excluding hidden files) and create an array containing the URL, its file size and its modification date. At the same time add up all file sizes. If the size of the folder is higher than the maximum cache size, sort the array using the modification date. Loop over the sorted array and remove files (starting from the oldest) till the folder size is small than the allowed maximum size:

- (void)cleanupOldFiles
{
    NSDirectoryEnumerator *dirEnumerator =
    [self.fileManager enumeratorAtURL:self.cacheFolder
           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}];
    }
    int maxCacheSize = FLICKRCACHE_MAXSIZE_IPHONE;
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        maxCacheSize = FLICKRCACHE_MAXSIZE_IPAD;
    }
    if (dirSize > maxCacheSize) {
        NSArray *sorted = [files sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
            return [obj1[@"date"] compare:obj2[@"date"]];
        }];
        [sorted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            dirSize -= [obj[@"size"] integerValue];
            NSError *error;
            [self.fileManager removeItemAtURL:obj[@"url"] error:&error];
            *stop = error || (dirSize < maxCacheSize);
        }];
    }    
}

… and use different image sizes depending on the current device:

- (void)sendDataforIndexPath:(NSIndexPath *)indexPath toViewController:(UIViewController *)vc
{
        ...
        NSURL *url = [FlickrFetcher urlForPhoto:self.photos[indexPath.row]
                                         format:([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? FlickrPhotoFormatOriginal : FlickrPhotoFormatLarge];
        ...
}

The complete code is available on github.

Facebooktwittergoogle_plusredditpinterestlinkedintumblrmailFacebooktwittergoogle_plusredditpinterestlinkedintumblrmail

Flattr this!

Leave a Reply

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