Ad

Our DNA is written in Swift
Jump

And Now Lazy Loading with NSURLConnection

In my previous post I demonstrated how to quickly whip up some lazy loading for a UIImageView. Today I revamped it to use NSURLConnection instead, because this would allow for cancelling the request. It also gives us the option of specifying if we want to make use of the cache and also how long we’re actually willing to wait for an answer.

That sounds more straightforward than it actually is. If you simply use an NSURLConnection with its delegate methods then you might not see anything wrong, unless you are using this lazy image view as subview of a UIScrollView. The problem there is that the scroll view blocks the run loop and so your connection events will not get delivered until you lift the finger.

But I found a solution for that. Let me know in the comments if you think something could be done in a more elegant way. The code for this is available in the NSAttributedString+HTML project.

DTLazyImageView.h

@interface DTLazyImageView : UIImageView 
{
	NSURL *_url;
 
	NSURLConnection *_connection;
	NSMutableData *_receivedData;
}
 
@property (nonatomic, retain) NSURL *url;
 
- (void)cancelLoading;
 
@end

DTLazyImageView.m

#import "DTLazyImageView.h"
 
 
@implementation DTLazyImageView
 
- (void)dealloc
{
	self.image = nil;
	[_url release];
 
	[_receivedData release];
	[_connection cancel];
	[_connection release];
 
	[super dealloc];
}
 
- (void)loadImageAtURL:(NSURL *)url
{
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
 
	NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url 
				cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
	_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self
				 startImmediately:YES];
	[request release];
 
	// so that our connection events get processed even if a scrollview blocks the main run loop
	CFRunLoopRun(); 
	[pool release];
}
 
- (void)didMoveToSuperview
{
	if (!self.image && _url && !_connection)
	{
		//[self loadImageAtURL:_url];
		[self performSelectorInBackground:@selector(loadImageAtURL:) withObject:_url];
	}	
}
 
- (void)cancelLoading
{
	[_connection cancel];
	[_connection release], _connection = nil;
 
	[_receivedData release], _receivedData = nil;
 
	CFRunLoopStop(CFRunLoopGetCurrent());
}
 
#pragma mark NSURL Loading
 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
	// every time we get an response it might be a forward, so we discard what data we have
	[_receivedData release], _receivedData = nil;
 
	// does not fire for local file URLs
	if ([response isKindOfClass:[NSHTTPURLResponse class]])
	{
		NSHTTPURLResponse *httpResponse = (id)response;
 
		if (![[httpResponse MIMEType] hasPrefix:@"image"])
		{
			[self cancelLoading];
		}
	}
 
	_receivedData = [[NSMutableData alloc] init];
}
 
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
	[_receivedData appendData:data];
}
 
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
	if (_receivedData)
	{
		UIImage *image = [[UIImage alloc] initWithData:_receivedData];
 
		//self.image = image;
		[self performSelectorOnMainThread:@selector(setImage:) withObject:image 
				waitUntilDone:YES];
 
		[image release];
 
		[_receivedData release], _receivedData = nil;
	}
 
	[_connection release], _connection = nil;
 
	CFRunLoopStop(CFRunLoopGetCurrent());
}
 
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
	NSLog(@"Failed to load image at %@, %@", _url, [error localizedDescription]);
 
	[_connection release], _connection = nil;
	[_receivedData release], _receivedData = nil;
 
	CFRunLoopStop(CFRunLoopGetCurrent());
}
 
 
#pragma mark Properties
 
@synthesize url = _url;
 
@end

The trick is to use CFRunLoopRun to have a new run loop to take care of delivering the events for this NSURLConnection. If you use that you also have to stop it if it’s no longer needed. There are now 3 places where this is the case: successful loading, error and canceling.

This also shows how you can check the MIME type of the received data, because you might want to cancel the operation if it is not an image you are getting back.

I found that I needed to combine this approach with running the NSURLConnection on a background thread because otherwise there would be unwanted side effects impacting the tracking in my UIScrollView. As I mentioned before, I’m on shaky territory as I have never messed with run loops before. So please tell me if there’s some problem with this approach.

… and so Nyxouf did, his suggestion in the comments allowed me to remove quite a few lines of this nasty code after I found this to be working just as well. The trick seems to be to not have the connection start right away, but first schedule it on the current run loop for the common modes and then start it.

DTLazyImageView.m

#import "DTLazyImageView.h"
 
 
@implementation DTLazyImageView
 
- (void)dealloc
{
	self.image = nil;
	[_url release];
 
	[_receivedData release];
	[_connection cancel];
	[_connection release];
 
	[super dealloc];
}
 
- (void)loadImageAtURL:(NSURL *)url
{
	NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url 
			cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
 
	_connection = [[NSURLConnection alloc] initWithRequest:request delegate:self 
			startImmediately:NO];
	[_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] 
			forMode:NSRunLoopCommonModes];
	[_connection start];
 
	[request release];
}
 
- (void)didMoveToSuperview
{
	if (!self.image && _url && !_connection)
	{
		[self loadImageAtURL:_url];
	}	
}
 
- (void)cancelLoading
{
	[_connection cancel];
	[_connection release], _connection = nil;
 
	[_receivedData release], _receivedData = nil;
}
 
#pragma mark NSURL Loading
 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
	// every time we get an response it might be a forward, so we discard what data we have
	[_receivedData release], _receivedData = nil;
 
	// does not fire for local file URLs
	if ([response isKindOfClass:[NSHTTPURLResponse class]])
	{
		NSHTTPURLResponse *httpResponse = (id)response;
 
		if (![[httpResponse MIMEType] hasPrefix:@"image"])
		{
			[self cancelLoading];
		}
	}
 
	_receivedData = [[NSMutableData alloc] init];
}
 
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
	[_receivedData appendData:data];
}
 
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
	if (_receivedData)
	{
		UIImage *image = [[UIImage alloc] initWithData:_receivedData];
 
		self.image = image;
 
		[image release];
 
		[_receivedData release], _receivedData = nil;
	}
 
	[_connection release], _connection = nil;
}
 
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
	NSLog(@"Failed to load image at %@, %@", _url, [error localizedDescription]);
 
	[_connection release], _connection = nil;
	[_receivedData release], _receivedData = nil;
}
 
 
#pragma mark Properties
 
@synthesize url = _url;
 
@end

Categories: Recipes

5 Comments »

  1. Did you try this :

    _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
    [_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [_connection start];

    Normally, you can scroll the scrollView without the connection being interrupted.

  2. My nice sample on GCD anync image downloader: http://iphone-dev-tips.alterplay.com/2011/05/grand-central-dispatch-magic.html GCD lets to keep UI smooth while doing heavy operations or set of operations, e.g. downloading and saving big files to disk.

  3. How can i create lazy loading which create thumbnail view…please give guidelines

  4. This is a little late, but I ran into this exact problem. I have a UIScrollView that scrolls through over 1000 thumbnail pictures… obviously, I wanted to load them in and out lazily while the view is scrolling. I have been racking my head over this, and your 3 lines of code solved everything!

    So all that to say, thank you so much!!!