If you have been following me on Twitter or reading MKBlog, you would already be knowing about MKNetworkKit.

I wrote a introductory post on MKNetworkKit couple of months ago and later explained in a more detailed post on how to use it in other sophisticated scenarios.

 

From feedback so far, Image Caching was one aspect of MKNetworkKit that developers didn’t understand pretty well. In this post, I’ll try to explain how to use MKNetworkKit for image caching and loading thumbnails.
Thumbnails are often used in iOS apps to load friend’s avatar pictures from a Facebook feed or flickr thumbnails for a given tag and in many similar cases.

MKNetworkEngine makes it a breeze to add this feature into your app. What not? With MKNetworkKit, you will even know if the response image is from cache or loaded for the first time. Using this information, you can “fade in” your thumbnails when you load the images for the first time (like in Apple’s iTunes Movie Trailers app).

The completed example looks like this.

IOS Simulator

Screenshot showing images loaded asynchronously using MKNetworkKit

Having said that, lets get our hands dirty with some real code. In this example, I’ll show you how to load images from Flickr on a table view controller asynchronously.

Step 1: Create a Flickr Engine

Easy peasy. Create a subclass of MKNetworkEngine and let’s name it as “FlickrEngine”. Initialize your flickr engine with api.flickr.com as hostname in your application delegate.

Step 2: Create a custom cache directory for storing the cached Flickr thumbanils

Override cacheDirectoryName and return a custom directory for storing the cached thumbnails.

-(NSString*) cacheDirectoryName {
 
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *cacheDirectoryName = [documentsDirectory stringByAppendingPathComponent:@"FlickrImages"];
    return cacheDirectoryName;
}

This step is optional. But, since thumbnails can easily occupy precious disk space, you might want to clear them at periodic intervals. MKNetworkEngine provides a handy method (emptyCache) to empty the cache directory for a specific engine. The default implementation of this method removes all files within the cache directory of that engine. If you don’t override cacheDirectoryName and provide a distinct cache directory for your engine, you will end up emptying the shared cache directory, when you call emptyCache.

Step 3: Write a method to list flickr images for a specified tag

By now, you already know that creating a network request using MKNetworkKit is a no-brainer. The following shows how to create a request for loading images for a given tag.

-(void) imagesForTag:(NSString*) tag onCompletion:(FlickrImagesResponseBlock) imageURLBlock onError:(MKNKErrorBlock) errorBlock {
 
    MKNetworkOperation *op = [self operationWithPath:FLICKR_IMAGE_URL([tag urlEncodedString])];
 
    [op onCompletion:^(MKNetworkOperation *completedOperation) {
 
        NSDictionary *response = [completedOperation responseJSON];
        imageURLBlock([[response objectForKey:@"photos"] objectForKey:@"photo"]);
 
    } onError:^(NSError *error) {
 
        errorBlock(error);
    }];
 
    [self enqueueOperation:op];
}

The FLICKR_IMAGE_URL macro is defined as

#define FLICKR_IMAGE_URL(__TAG__) [NSString stringWithFormat:@"services/rest/?method=flickr.photos.search&api_key=%@&tags=%@&per_page=200&format=json&nojsoncallback=1", FLICKR_KEY, __TAG__]

Note that, the host name api.flickr.com gets prefixed automatically when you create operations on this engine.

You should generate a new flickr api key and replace it in the FLICKR_KEY macro. You can create one for free here. (For running the example, you can use my key)

Step 4: Create a custom table view cell for loading flickr images

In this custom cell, write a method setFlickrData: that updates the cell from the dictionary you receive by invoking the flickr REST API

-(void) setFlickrData:(NSDictionary*) thisFlickrImage {
 
    self.titleLabel.text = [thisFlickrImage objectForKey:@"title"];
	self.authorNameLabel.text = [thisFlickrImage objectForKey:@"owner"];
    self.loadingImageURLString =
    [NSString stringWithFormat:@"http://farm%@.static.flickr.com/%@/%@_%@_s.jpg",
     [thisFlickrImage objectForKey:@"farm"], [thisFlickrImage objectForKey:@"server"],
     [thisFlickrImage objectForKey:@"id"], [thisFlickrImage objectForKey:@"secret"]];
 
    self.imageLoadingOperation = [ApplicationDelegate.flickrEngine imageAtURL:[NSURL URLWithString:self.loadingImageURLString]
                                    onCompletion:^(UIImage *fetchedImage, NSURL *url, BOOL isInCache) {
 
                                        if([self.loadingImageURLString isEqualToString:[url absoluteString]]) {
                                                self.thumbnailImage.image = fetchedImage;
                                    }];
}

Tip: Avoid setting the individual labels of a table view cell in the cellForRowAtIndexPath: method. It makes the cell less portable across your other view controllers.

Carefully note the use of the variable self.loadingImageURLString. If you don’t save the loading image’s URL in an ivar, your images will continue to be updated when the previous operation completes. To circumvent this, either cancel the operation when a cell is reused, or check if the returned URL is the original URL that was loaded for this cell (by storing it in ivar).

As on this commit, MKNetworkEngine’s imageAtURL method will return you the actual operation created. You should retain this and cancel it when not necessary.

Step 5: Clear previously loaded images if the cell will be reused

You should clear previously loaded images by overriding prepareForReuse method. It’s a one-liner.

-(void) prepareForReuse {
 
    self.thumbnailImage.image = nil;
    [self.imageLoadingOperation cancel];
}

If you don’t write this, you will see old images on the cell while the new image is being loaded.
Instead of “nil” you can also show a default placeholder image here.

You should also consider canceling the old imageLoadingOperation so as to load the latest visible cells faster. If you don’t cancel the operation, when the user scrolls fast to the bottom of the list, the engine loads all the images from the first cell (including images for cells that are now hidden and reused) and there by slowing down the image load operation for the currently visible cell. You can test this from the code by commenting it out and checking the performance.

Step 6: Fade in thumbnails like the Apple trailers app

MKNetworkEngine’s imageAtURL method returns the image in a block method. The block method, onCompletion looks like this.

^(UIImage *fetchedImage, NSURL *url, BOOL isInCache) {
 
                                        if([self.loadingImageURLString isEqualToString:[url absoluteString]]) {
                                                self.thumbnailImage.image = fetchedImage;
                                    }];
}

If the variable isInCache is false, you can “fade in” the image so as to add a nice UI touch to your app. The following code block illustrates this.

^(UIImage *fetchedImage, NSURL *url, BOOL isInCache) {
 
                                        if([self.loadingImageURLString isEqualToString:[url absoluteString]]) {
 
                                            if (isInCache) {
                                                self.thumbnailImage.image = fetchedImage;
                                            } else {
                                                UIImageView *loadedImageView = [[UIImageView alloc] initWithImage:fetchedImage];
                                                loadedImageView.frame = self.thumbnailImage.frame;
                                                loadedImageView.alpha = 0;
                                                [self.contentView addSubview:loadedImageView];
 
                                                [UIView animateWithDuration:0.4
                                                                 animations:^
                                                 {
                                                     loadedImageView.alpha = 1;
                                                     self.thumbnailImage.alpha = 0;
                                                 }
                                                                 completion:^(BOOL finished)
                                                 {
                                                     self.thumbnailImage.image = fetchedImage;
                                                     self.thumbnailImage.alpha = 1;
                                                     [loadedImageView removeFromSuperview];
                                                 }];
                                            }
                                        }
                                    }

Source Code

The complete source code is available as a demo on MKNetworkKit’s Github repository.

Mugunth

Follow me on Twitter