Monday, July 2, 2012

Playing a bit with NSURLConnection

First of all, I just have to define what I'm going to do. I found an open API to work with. I'm a WOW player and since there's an API by Blizzard to access some WOW related information, I'm going to use that to get specific WOW character data.

Here's the API doc, if you're interested:
http://blizzard.github.com/api-wow-docs/

Since I want to use "new" stuff (by new I mean thing I never used before) I will use ARC, GCD with blocks, native JSON parsing, I'm going to use storyboard and I want to put as most IOS version >= 4.0 features as I can.

First we need a HTTP client to work with. I choose NSURLConnection because the delegate callbacks are close to what I want to have.


Interface


So, what are the requirements?
To keep things simple I want to have a delegate to implement just the necessary things
@protocol HTTPRequestServiceResponseDelegate <NSObject> 
@required
- (void)HTTPConnectionFinishedWithData:(NSData*)data;
- (void)HTTPConnectionFailedWithError:(NSError*)error;
@optional
- (void)HTTPStatusCodeFetched:(int)statusCode;
- (void)HTTPConnectionCancalled;
@end

Fairly simple right? The HTTPConnectionFinishedWithData method will send the downloaded data to the delegate, while the HTTPConnectionFailedWithError method will notify the delegate on errors. The optional methods are just to have additional informations if you want to have those.
Let's start implementing stuff. My HTTP client will encapsulate the NSURLConnection and it's related vars and will give back feedback and data to it's delegate. My plan was to initialize with a host and with two optional parameters, HTTP method and variables in a dictionary.

The initialization looks like this.
- (id)initWithHost:(NSString*)host;
- (id)initWithHost:(NSString*)host withParameters:(NSDictionary*)parameters;
- (id)initWithHost:(NSString*)host withParameters:(NSDictionary*)parameters withHTTPMethod:(NSString*)method;

I wanted to keep the number of ivars as less as possible. The most important is the responseData where the callback will gather the connection's data. The other ivars are to keep track of the connection and store some specific data if it's required later on. Plus we have the connection delegate, where we can send back our data/feedback.
@interface HTTPRequestService : NSObject <NSURLConnectionDelegate>  {
    NSURL * requestURL;
    NSURLConnection * connection;
    NSString * HTTPMethod;
    NSMutableData * responseData;
    BOOL running;
    BOOL cancalled;
}

@property (weak) id<HTTPRequestServiceResponseDelegate> delegate;

@property (nonatomic, strong) NSURL * requestURL;
@property (nonatomic, strong) NSURLConnection * connection;
@property (nonatomic) BOOL running;
@property (nonatomic) BOOL cancalled;

@property (nonatomic, strong) NSString * HTTPMethod;
@property (atomic, strong) NSMutableData * responseData;

I added a few other methods to my implementation. These are:
- (BOOL)validateURL;
- (BOOL)startRequest;
- (void)cancelRequest;

The validateURL method is self explanatory as well as the startRequest method. The cancelRequest will cancel an ongoing request and it will notify the delegate.
You can see there there that I was using ARC, so I used strong/weak references. The only thing worth mentioning here is that you always use only weak on delegates.

Implementation

- (id)initWithHost:(NSString*)host withParameters:(NSDictionary*)parameters withHTTPMethod:(NSString*)method {
    self = [super init];
    if (self) {
        self.HTTPMethod = method;
        
        if ([host length]) { // Only doing anything if have at least a host to do a request with
            NSString * HTTPParameters = nil;
            if ([parameters count]) {
                HTTPParameters = [URLUtil createQueryStringFromDictionary:parameters];
            }
            NSMutableString * urlString = [NSMutableString stringWithString:host];
            if ([HTTPParameters length]) {
                [urlString appendString:HTTPParameters];
            }
            NSLog(@"URL to call: %@", urlString);
            self.requestURL = [NSURL URLWithString:urlString];
        }
    }
    return self;
}

This is the point where we got all the required info to do the actual initialization. We're doing some validation on the parameters, and using the URLUtil we put together the parameters string from the dictionary. Note here that the request has not been fired here, we're just dealing with the url.

A bit about the URLUtil. This little class is to convert our dictionary into a parameter string. Pretty easy stuff, I'm not going to explain it in details, you can just check the code. It will iterate through the dictionary, escape it and it will return a string ready to use for NSURL.
@implementation URLUtil

+(NSString*)urlEscapeString:(NSString *)unencodedString 
{
    CFStringRef originalStringRef = (__bridge_retained CFStringRef)unencodedString;
    NSString *s = (__bridge_transfer NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,originalStringRef, NULL, NULL,kCFStringEncodingUTF8);
    CFRelease(originalStringRef);
    return s;
}

+ (NSString*)createQueryStringFromDictionary:(NSDictionary *)dictionary
{
    NSMutableString *urlWithQuerystring = [NSMutableString string];
    
    for (id key in dictionary) {
        NSString *keyString = [key description];
        NSString *valueString = [[dictionary objectForKey:key] description];
        
        if ([urlWithQuerystring rangeOfString:@"?"].location == NSNotFound) {
            [urlWithQuerystring appendFormat:@"?%@=%@", [self urlEscapeString:keyString], [self urlEscapeString:valueString]];
        } else {
            [urlWithQuerystring appendFormat:@"&%@=%@", [self urlEscapeString:keyString], [self urlEscapeString:valueString]];
        }
    }
    return [urlWithQuerystring stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}

@end

After our initialization is done, we can validate the URL with the validateURL method and you can fire the request using the startRequest method which will give you back NO if there was any error during the creation of the connection.
- (BOOL)validateURL {
    if (self.requestURL != nil) {
        return YES;
    }
    return NO;
}

- (void)cancelRequest {
    if (self.running) {
        [self.connection cancel];
        self.running = NO;
        self.cancalled = YES;
        if ([delegate respondsToSelector:@selector(HTTPConnectionCancalled)]) {
            [delegate HTTPConnectionCancalled];
        }
    }
}

- (BOOL)startRequest {
    if (self.requestURL) {
        NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:requestURL];
        [request setHTTPMethod:self.HTTPMethod];
        self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
        responseData = [NSMutableData data];
        self.running = YES;
        [connection start];
        return YES;
    }
    else {
        NSLog(@"Cannot create NSURL from urlString");
    }
    return NO;
}

Data handling

At this point if we did everything right, we have an open HTTP connection. We have to accept the data coming through and we have to pass the relevant information to our delegate.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response {
    NSHTTPURLResponse * HTTPResponse = (NSHTTPURLResponse *)response;
    int statusCode = [HTTPResponse statusCode];
    if ([delegate respondsToSelector:@selector(HTTPStatusCodeFetched:)]) {
        [delegate HTTPStatusCodeFetched:statusCode];
    }
}

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

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    if (delegate) {
        [delegate HTTPConnectionFinishedWithData:responseData];
    }
    self.running = NO;
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [delegate HTTPConnectionFailedWithError:error];
}

The first method (didReceiveResponse) will be responsible for giving back an HTTP status code. The didReceiveData is again self explanatory as well as the didFailWithError method. After the connection is closed we'll pass back the data using the HTTPConnectionFinishedWithData method.

That's all for now, feel free to add any constructive comment. You can download the source code here.

No comments:

Post a Comment