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

  • http://twitter.com/fabienpenso Fabien Penso

    What if you need to fetch images from multiples hosts (unlimited), how can you create an engine without specifying the host, and have it per request?

    • Anonymous

      You can use operationWithURLString instead of operationWithPath method. operationWithURLString creates an operation with an absolute URL.

      Mugunth
      Author | Developer | Trainer
      iOS 5 Programming book
      iBooks: http://mk.sg/ibook
      Amazon: http://mk.sg/ios5book

  • Demosc

    [sorry for my poor english]

    Hi,

    I have a doubt about wich is the best approach.
    Sometime ago i saw a project using ASIHttprequest, the project used a singleton class with a lot of connections and using tags to diferenciate them, in the singleton there was some methods to parse the response and then diferents method wich call the models, and fill with the parse data.

    I liked this approach but i always have doubt if this is the best way to call a web service.
    Is this approach good? which is the best way to do with your library?

    Can yo do a post explaining this things?

    (Proposal: yesterday i bought your book and realize that you use ASI, maybe you can rewrite this chapter with your library and send the chapter who bought your book (the people who bought it must send you a picture of the book))

    • Anonymous

      I used to use ASI before it’s plug was pulled.
      The reason I created this new framework was.,
      I needed a fast, ARC based framework

      On your question on singleton., avoid them.
      Read my post about singletons here

      http://blog.mugunthkumar.com/articles/singleton-aint-bad-after-all/

      • Demosc

         Thanks for the info.

        I have another doubt, is about Mknetworkkit iOS demo, why you put all the instantation of engines in the delegate (yahooEngine, flickrEngine, samplePoster…).
        I tought that this is a bad practice (all the people in the forums said that).

        • Anonymous

          Only shared data shouldn’t be there in AppDelegate.
          Classes used globally like this and Core Data related classes can be and should be in AppDelegate instead of a singleton.

          Even the built in Core data template generated by Xcode adds the managedObjectContext and persistenceStoreCoordinator in AppDelegate

          Just ensure that you don’t use AppDelegate to share data across view controllers

          • Demosc

             Thanks for the answers.

            Any chance to rewrite iHotelApp (from your book) with mknetworkkit?

          • Demosc

             http://www.hollance.com/2012/02/dont-abuse-the-app-delegate/

  • jan

    I tried running your code, but i keep getting this error, any clue why is this ?

  • shane

    I tried running your code, but i keep getting this error, any clue why is this ?

    ld: library not found for -lMKNetworkKit-iOS
    Command /Developer/Platforms/iPhoneSimulator.platform/Developer/usr/bin/clang failed with exit code 1

    • Anonymous

      You should open the workspace and not the individual project.

  • Christian

    Hi and thanks for one more great tutorial.

    Do you think it is possible to make one showing the current state of https communication. would you recommend using it for a https API, as you state in your TODO list on github, that this needs more testing:
    Fix and test Client certificate/Server trust auth

    Once more: Thanks for this great framework and regards,
    Christian

  • Nelson Tai

    Thanks for your good job!

    I have a question:
    If I request two image URLs, and they are both redirected to the same image URL.
    Does MKNetworkKit keep only one cache for this image, or does it keep two caches?

    Regards,
    Nelson

    • MugunthKumar

      Just one cache.
      The cache stores the final URL and not the original URL

      • Nelson Tai

         Sorry for bothering you again, I’ve replied a comment indicating that caches seem to be duplicated. Did you receive that? The comment not display in the thread yet.

        • MugunthKumar

          I got that comment. Looking at the issue. But is it deal breaking?

  • http://twitter.com/EvGeniyLell EvGeniyLell

    How can I repeat the MKNetworkOperation several times during after failure?[operation onCompletion:^(MKNetworkOperation *operation) {DLog(@"%@", operation);} onError:^(NSError *error) {DLog(@"%@", error);[self enqueueOperation:operation];// not work}];

  • Paulo

    Hi,
    How should I do to download an image and save it, to use it later, like a photo album?
    Thanx

  • those

    Hello, 

    I get this error on the console: imageAtURL:onCompletion: requires caching to be enabled.

    what does it mean? 

    thanks

    • Pym

      I get this error too. and i already set cacheDirectoryName .

      why isCacheEnabled is return false?

      Thanks

      • MugunthKumar

        You should call useCache on the engine before calling this method. Otherwise, imageAtURL will still work, but efficiency will be poor.

        Mugunth
        Author | Developer | Trainer
        iostraining.sg
        Preorder the iOS 6 Programming Pushing the Limits book http://mk.sg/ios6book

        • Pym

          Thanks , MugunthKumar 

          it work now,

  • ibjazz

    Will This work also for videos?

  • Nelson Tai

    I’m new to Objective-C and I have a question about your code snippet.

    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;
    }];

    You use “self” in the block, doesn’t it cause retain cycle?Shouldn’t you use “__weak (MyClass *)weakSelf = self” and use “weakSelf” in the block?

  • http://www.facebook.com/jerometonnelierpro Jérôme Tonnelier

    Really awesome job!!! Congrats, it’s very usefull.

    I had a question though : I download images (thumbs and full size images), but I don’t have the image sizes before downloading them. The thing is if I set a target size, images are scaled with ScaleToFill behavior instead of a ScaleToFit. I could modify the library, but I feel bad about it ;-) Did you though of this and have a clue, or should I just go and modify decompressedResponseImageOfSize?

    thx

    • MugunthKumar

      Hmm interesting, I think I should add a parameter for that.

      • dunken

        Hi there,

        I came across a similar situation as Jerome above.

        I set a UIImageview object and give it an url to load from, using also cache.

        Even if UIImageView’s contentMode is set to UIViewContentModeScaleAspectFit, image is not proportioned correctly.

        Do you have a fix for this?

        Thanks,

      • http://www.facebook.com/jerometonnelierpro Jérôme Tonnelier

        Thanks a lot :-)