Loading an Image Asynchronously in iOS

by

If you had read thru Apple’s iOS Human Interface Guidelines, I’m sure you’d notice that it mentioned about how your app should provide a responsive user experience. If the app is processing a lengthy operation, it ought to provide a feedback as suggested by Apple, “Subtle animation can give people meaningful feedback..” Consider this lengthy operation:  loading image from a URL. This is a very common task. The problem is, UIImageView doesn’t have a built-in method that lets you load an image and at the same time give you a chance to provide a meaningful UI feedback such as a “subtle animation”.

You can easily load an image from URL this way:

UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://blog.grio.com/wp-content/uploads/2013/04/mountain_lion_stamp.png"]]];

This works. Just not like the way Apple suggests. When the code on line 2 executes, it blocks the app from doing anything else except downloading the image. This is by no means a quick operation. It may take several seconds depending on network speed and image size. During this time, your user is blocked and probably doesn’t know what’s going on. You can try to put UIActivityIndicatorView, but the spinner won’t show up nor spin. Add animation and it won’t animate. Very bad user experience.

I’ve quickly written a class that loads image from a URL but does it in a non-blocking manner. It leverages NSURLConnection class to perform all the network connectivity tasks and listens to the network event callbacks (implementing NSURLConnectionDelegate protocol). The class is called URLImageView which extends from UIImageView. It has only 2 additional functions to kick off the image loading:

- (void)startLoading:(NSString*)url;
- (void)startLoading:(NSString*)url completion:(void (^)(BOOL finished))completion;

The second method allows you to supply a completion block which will execute when the image is loaded completely. Here’s the code to replace the aforementioned blocking snippet:

URLImageView *imageView = [[URLImageView alloc] init];
[imageView startLoading:@"http://blog.grio.com/wp-content/uploads/2013/04/mountain_lion_stamp.png"];

You’d notice that when the loading starts, an activity indicator spinner will appear and continue to spin until the loading is completed. Here’s the complete code:

//
//  URLImageView.h
//
@interface URLImageView : UIImageView  {
    NSMutableData *_receivedData;
    UIActivityIndicatorView *_loadingIndicatorView;
    void (^_completionBlock)(BOOL finished);
}

@property BOOL suppressLoadingIndicator;

- (void)startLoading:(NSString*)url completion:(void (^)(BOOL finished))completion;
- (void)startLoading:(NSString*)url;
@end

 

//
//  URLImageView.m
//

#import "URLImageView.h"

@implementation URLImageView
//
// Start loading the image whose URL is specified in url
//
- (void)startLoading:(NSString*)url {
    [self startLoading:url completion:nil];
}

- (void)startLoading:(NSString*)url completion:(void (^)(BOOL finished))completion {
    if (!url)
        return;

    _completionBlock = completion;

    if (!self.suppressLoadingIndicator) {
        _loadingIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        [self addSubview:_loadingIndicatorView];
        _loadingIndicatorView.center = self.center;
        [_loadingIndicatorView startAnimating];
    }

    _receivedData = [[NSMutableData alloc] init];
    NSURLConnection *connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]
                                                                delegate:self];
    [connection start];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [_receivedData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [_receivedData appendData:data];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    if (_completionBlock) {
        _completionBlock(NO);
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    UIImage *image = [[UIImage alloc] initWithData:_receivedData];

    if (_loadingIndicatorView) {
        [_loadingIndicatorView stopAnimating];
        [_loadingIndicatorView removeFromSuperview];
        _loadingIndicatorView = nil;
    }

    if (!image) {
        if (_completionBlock) {
            _completionBlock(NO);
        }
    }
    else {
        self.image = image;
        if (_completionBlock) {
            _completionBlock(YES);
        }
    }
}
@end

5 Comments

  1. I think you wanna use AFNetworking setImageWithURLRequest with a really nice callback block and avoid useless code.
    You code works with few images but it does a poor job with several images.

Leave a Reply

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