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.