Friday, July 17, 2009

 

HttpHelper

...it's sort of like Hamburger Helper, only tastier.

It's been a week of tweaking, fixing little bugs, spiralling in towards hoped-for perfection. I'm pleased to report that I now have an alpha-test version of my iPhone app up and running on my iPod Touch, and it looks to work a charm. If all goes very well indeed I might submit it to the App Store - where it will ultimately be available for the low low price of $0.00 - by the end of the month.

In the meantime, here's a utility class that people might find handy: my HttpHelper singleton. It's not really a class variable, since it seems Objective-C doesn't support them (Oh, Smalltalk, how I miss your class instance variables) but "static" has the same effect, if you code carefully. Like so:


//
// HttpHelper.m
// iTravelWrite
//
// Singleton class used to send HTTP requests and forward responses to selectors passed in by the caller.
// Created by Jon Evans on 04/07/09.
//

#import "HttpHelper.h"
#import "Util.h"

@implementation HttpHelper

static HttpHelper *singleton=nil;

#pragma mark -
#pragma mark Singleton methods

+(HttpHelper *) getInstance {
if (singleton==nil)
singleton = [[HttpHelper alloc] init];
return singleton;
}

+(id)allocWithZone:(NSZone *)zone {
if (singleton == nil) {
singleton = [super allocWithZone:zone];
return singleton;
}
return nil;
}

-(id)copyWithZone:(NSZone *)zone {
return self;
}

-(id)retain {
return self;
}

-(unsigned)retainCount {
return UINT_MAX;
}

-(void)release {
//pass
}

-(id)autorelease {
return self;
}

- (void)dealloc {
[super dealloc];
}


All the above is housekeeping stuff to ensure that we only ever have one instance of an HttpHelper. Not that a duplicate would be so disastrous in this case, but hey, if you're writing a singleton, write a singleton, right?

Here's the part where it actually does stuff. In particular, it does all the HTTP GETs and HTTP POSTs that your iPhone app will ever need:


#pragma mark -
#pragma mark Business logic

+(NSURLRequest*) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString {

NSMutableString *params=[[NSMutableString alloc] initWithCapacity:1024];
for (int i=0; i<[postValues count]; i++) {
[params appendString:[postKeys objectAtIndex:i]];
[params appendString:@"="];
[params appendString:[postValues objectAtIndex:i]];
[params appendString:@"&"];
}
NSData * paramData = [params dataUsingEncoding:NSUTF8StringEncoding];

NSURL * url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = NSURLRequestReloadIgnoringCacheData; // never cache a post response, at least in my app
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:10.0];

NSString *msgLength = [NSString stringWithFormat:@"%d", [paramData length]];
[request addValue: msgLength forHTTPHeaderField:@"Content-Length"];
[request setHTTPMethod:@"POST"];
[request setHTTPBody: paramData];
return request;
}

+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {

NSArray *keys = [NSArray arrayWithObjects:@"request", @"caller", @"onSuccess", @"onFailure", nil];
NSArray *values = [NSArray arrayWithObjects:request, caller,
[NSValue valueWithBytes:&onSuccess objCType:@encode(SEL)],
[NSValue valueWithBytes:&onFailure objCType:@encode(SEL)],
nil];
NSDictionary *args = [NSDictionary dictionaryWithObjects:values forKeys: keys];
NSThread* uploadThread = [[NSThread alloc] initWithTarget:[self getInstance] selector:@selector(doHttp:) object:args];
[uploadThread start];
[uploadThread release];
return TRUE;
}

+(NSURLRequestCachePolicy)getCachePolicyFor:(NSString *)urlString {
/*
Hived out to a separate method because we might fine-tune this later.

In theory, this will cause the app to use the cache policies set by
wetravelwrite.appspot.com, which as of this writing means 1 hour
for listing information, and 10 hours for searches.

In the ListingEditController and ListingsViewController we manually wipe
the cache for listings edited by the app.
*/
// return NSURLRequestUseProtocolCachePolicy;
return NSURLRequestReloadIgnoringCacheData;
}

+(NSURLRequest *)getURLRequestFor:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = [self getCachePolicyFor:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:7.5];
return request;
}

+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {
NSURLRequest *request = [self getURLRequestFor:urlString];
NSArray *keys = [NSArray arrayWithObjects:@"request", @"caller", @"onSuccess", @"onFailure", nil];
NSArray *values = [NSArray arrayWithObjects:request, caller,
[NSValue valueWithBytes:&onSuccess objCType:@encode(SEL)],
[NSValue valueWithBytes:&onFailure objCType:@encode(SEL)],
nil];
NSDictionary *args = [NSDictionary dictionaryWithObjects:values forKeys: keys];
NSThread* uploadThread = [[NSThread alloc] initWithTarget:[self getInstance] selector:@selector(doHttp:) object:args];
[uploadThread start];
[uploadThread release];
return TRUE;
}

-(BOOL) doHttp:(NSDictionary *)args
{
@synchronized (self) {
//autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSURLRequest *request = [args objectForKey:@"request"];
NSObject *caller = [args objectForKey:@"caller"];

SEL onSuccess;
[[args objectForKey:@"onSuccess"] getValue:&onSuccess];
SEL onFailure;
[[args objectForKey:@"onFailure"] getValue:&onFailure];

NSError *error;
NSData *returnData = [NSURLConnection sendSynchronousRequest: request returningResponse: nil error: &error ];
NSString * responseString = [[NSString alloc] initWithData: returnData encoding: NSASCIIStringEncoding];

if (!error) //connection succeeded; but did it work?
error = [Util getErrorFrom:responseString forRequest:request];

if (error) {
NSLog(@"iTravelWrite Error ",error);
[caller performSelectorOnMainThread: onFailure withObject:error waitUntilDone:NO];
}
else
[caller performSelectorOnMainThread: onSuccess withObject:responseString waitUntilDone:NO];
[pool release];
}
return TRUE;
}

@end


Pretty slick, huh?

The three chief interface methods are, for a GET -


+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure


and for a POST -


+(NSURLRequest*) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString

+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {


I'll give you an example of the former first, as it's easier. The call itself is perfectly straightforward:


NSMutableString *urlString = [NSMutableString stringWithCapacity:128];
[urlString appendString:[Util getSearchPageURL]];
[urlString appendString:@"?locale="];
[urlString appendString:[UserSettings getLanguage]];
[urlString appendString:@"&searchTerms="];
[urlString appendString:[searchBar.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[HttpHelper doGet:urlString forCaller:self onSuccess:@selector(parseSearchResults:) onFailure:@selector(searchFailed:)];


Not, however, the two "@selector" arguments. These must be methods on the caller, and they better expect an NSString* and an NSError*, respectively, as arguments. (Or they can take an NSObject* and cast from there, I suppose, but why bother, right?)

Inside HttpHelper we have to wrap the selectors in NSValues to pass them from method to method, which is a bit annoying, but hey, we only have to do it once.

To call a POST, by comparison:

NSArray *postKeys = [NSArray arrayWithObjects:@"title", @"location", @"comments", @"pageUri", @"sectionName", @"sectionNumber", @"listingName", nil];
NSArray *postValues = [NSArray arrayWithObjects:note.title, [note locationString], note.body, note.pageUri, note.sectionName, note.sectionNumber, note.listingName, nil];
NSURLRequest *request = [HttpHelper buildRequestWithPostKeys:postKeys postValues:postValues urlString:[Util getUploadNoteURL]];
[HttpHelper doPost:request forCaller:self onSuccess:@selector(onUploadSuccess) onFailure:@selector(onUploadError:)];


Note that "onUploadSuccess" here doesn't take an argument - I don't care about the web site's response, the fact of success is all that matters. OK, dubious wisdom that, but the example is relevant to show that the selector methods don't actually have to accept arguments.


We launch a new thread every time we call HttpHelper, so we have to be careful lest we run into concurrency problems. So we synchronize the actual HTTP calls in the "doHttp:" method, which is the one place in the app where we actually go out and connect to the big bad scary Internet.

Note also that if we wanted to change from using "sendSynchronousRequest:" to the delegated version of NSURLConnection, the only class that would change is HttpHelper. Ah, encapsulation.

Anyway. Share and enjoy, as the man said. Hope all that's useful to someone out there...

Labels: , , , , , , , , , , , ,


Comments:

Post a Comment

Subscribe to Post Comments [Atom]





<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]