NSURLConnection Helper

Jeff has got a good post up on the importance of using the iPhone’s built-in asynchronous communication classes and methods in lieu of threads:

Don’t spawn threads for asynchronous communications unless you would have used threads in the same situation if there was no network code.

Jeff argues for this position on the basis of overhead. I would make another argument: threading is too hard to get right to be worth the trouble most of the time. (I say this as someone who likes writing multithreaded code.) The difficultly of getting multithread code right is exponential in its complexity, and software has a way of always turning out a bit more complex than you might have expected.

Today I want to present a little helper class for NSURLConnection. If you’re going to use this class asynchronously, you’re going to need to supply a delegate, and it can be a minor nuisance to come up with one when you start working with this class. The MyNSURLConnectionDelegate class presented below should get you started.

Concept

The MyNSURLConnectionDelegate class is intended to provide a simple, immediately usable delegate implementation for asynchronous NSURLConnections. It would be used something like this:

@implementation SomeClass

// … snip …

// Set up and return (so that you can cancel it, etc.) an NSURLConnection
- (NSURLConnection*)startAsynchronousOperation
{
	NSURLRequest*	request = nil;
	id		context = nil;

	// Set up request and context to taste
	// ...

	MyNSURLConnectionDelegate* delegate = [[[MyNSURLConnectionDelegate alloc] initWithTarget:self
											  action:@selector(handleResultOrError:withContext:)
											 context:context] autorelease];

	NSURLConnection* conn = [NSURLConnection connectionWithRequest:request delegate:delegate];
	[conn start];
	return conn;
}

// Handle the result of an NSURLConnection.  Invoked asynchronously.
- (void)handleResultOrError:(id)resultOrError withContext:(id)context
{
	if ([resultOrError isKindOfClass:[NSError class]])
	{
		// Handle error
		// ...

		return;
	}

	NSURLResponse* response = [resultOrError objectForKey:@"response"];
	NSData* data = [resultOrError objectForKey:@"data"];

	// Handle response and data
	// ...
}

// … snip …

@end

The only slightly weird thing about MyNSURLConnectionDelegate is that it retains its target; this is unusual, but done to reflect the fact that NSURLConnection is itself weird in retaining its delegate (until it’s finished loading).

The context object is intended to wrap user-defined data; it has no meaning to MyNSURLConnectionDelegate.

Header

Here’s the header file:

#import <Foundation/Foundation.h>


@interface MyNSURLConnectionDelegate : NSObject
{
	NSURLResponse*	response;
	NSMutableData*	responseData;
	
	id		target;
	SEL		action;
	id		context;
}

- (id)initWithTarget:(id)target action:(SEL)action context:(id)context;

- (BOOL)connection:(NSURLConnection*)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace*)protectionSpace;
- (void)connection:(NSURLConnection*)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge;
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error;
- (void)connection:(NSURLConnection*)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge;
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data;
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response;
- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite;
- (NSCachedURLResponse*)connection:(NSURLConnection*)connection willCacheResponse:(NSCachedURLResponse*)cachedResponse;
- (NSURLRequest*)connection:(NSURLConnection*)connection willSendRequest:(NSURLRequest*)request redirectResponse:(NSURLResponse*)redirectResponse;
- (void)connectionDidFinishLoading:(NSURLConnection*)connection;
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection*)connection;

@end

This class doesn’t reference NSURLConnectionDelegate because the latter entity is declared as a category on NSObject (instead of as, for example, a protocol). I declare and define all relevant methods (per the documentation) in this class even though, for most of them, the implementation is minimal.

I don’t declare or define the connection:needNewBodyStream: method, which isn’t well documented, and mentioned only in one obscure corner of the Foundation Constants Reference.

Implementation

Here’s the skeleton of the implementation file:

#import "MyNSURLConnectionDelegate.h"


@interface MyNSURLConnectionDelegate ()

@property (nonatomic, retain) NSURLResponse* response;
@property (nonatomic, retain) NSMutableData* responseData;

@end


@implementation MyNSURLConnectionDelegate

@synthesize response;
@synthesize responseData;


- (id)init
{
	return [self initWithTarget:nil action:(SEL)0 context:nil];
}


- (id)initWithTarget:(id)a_target action:(SEL)a_action context:(id)a_context
{
	if (self = [super init])
	{
		target	= [a_target retain];
		action	= a_action;
		context	= [a_context retain];
	}
	return self;
}


- (void)dealloc
{
	[response release];
	[responseData release];
	[target release];
	[context release];
	[super dealloc];
}

// … snipped out the actual delegate methods …

@end

There isn’t much to see here; I define some convenience accessors in an extension, and, as I mentioned above, retain the target object.

Now, let’s move on to the actual delegate methods. Many of them are designed to mimic the behavior you’d get from an NSURLConnection if its delegate didn’t implement the relevant method at all, and all of them perform some logging, which is intended to aid in development.

- (BOOL)connection:(NSURLConnection*)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace*)protectionSpace
{
	NSLog(@"canAuthenticateAgainstProtectionSpace");
	if ([protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]	||
	    [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]		)
	{
		return NO;
	}
	else
	{
		return YES;
	}
}

This method’s somewhat obscure logic is designed to mimic the behavior of an NSURLConnection with a delegate that doesn’t implement this method.

- (void)connection:(NSURLConnection*)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
{
	NSLog(@"didCancelAuthenticationChallenge");
}

No action is required in this method, so we simply log it.

- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
	NSLog(@"didFailWithError");
	[target performSelector:action withObject:error withObject:context];
}

On failure, we pass the underlying error to our target/action pair.

- (void)connection:(NSURLConnection*)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
{
	NSLog(@"didReceiveAuthenticationChallenge");
	[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}

This class contains no logic to handle authentication, so we instruct the underlying sender to continue without credentials. (If we implement this method but take no action on receipt of an authentication challenge, the NSURLConnection will hang forever.)

- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
	NSLog(@"didReceiveData");
	[responseData appendData:data];
}

On receipt of data, append it to responseData. (I assume that at least one connection:didReceiveResponse: message is guaranteed to be sent before any connection:didReceiveData: messages; this interpretation of the contract may not be valid. If it’s wrong, responseData may be nil, and data handled by this method may be dropped on the floor. Caveat coder.)

- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)aResponse
{
	NSLog(@"didReceiveResponse");
	self.response = aResponse;
	self.responseData = [NSMutableData data];
}

On receipt of a response, store it and create a new NSMutableData object to hold data associated with that response.

- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
	NSLog(@"didSendBodyData");
}

No action is required in this method, so we simply log it.

- (NSCachedURLResponse*)connection:(NSURLConnection*)connection willCacheResponse:(NSCachedURLResponse*)cachedResponse
{
	NSLog(@"willCacheResponse");
	return cachedResponse;
}

This class will simply allow the proposed data to be cached.

- (NSURLRequest*)connection:(NSURLConnection*)connection willSendRequest:(NSURLRequest*)request redirectResponse:(NSURLResponse*)redirectResponse
{
	NSLog(@"willSendRequest (from %@ to %@)", redirectResponse.URL, request.URL);
	return request;
}

This class will simply allow the proposed redirect.

- (void)connectionDidFinishLoading:(NSURLConnection*)connection
{
	NSLog(@"connectionDidFinishLoading");
	[target performSelector:action
				 withObject:[NSDictionary dictionaryWithObjectsAndKeys:response,@"response",responseData,@"data",nil]
				 withObject:context];
}

On success, we wrap the response and its data into an NSDictionary and pass that, along with the user-supplied context, to our target/action pair.

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection*)connection
{
	NSLog(@"connectionShouldUseCredentialStorage");
	return YES;
}

This class always allows access to credential storage.

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.

Comments are closed.