Wednesday, October 19, 2011

Asynchronous web service client using NSURLConnection and SBJSON

The NSURLConnection class
A NSURLConnection object is used to perform the execution of a web service using HTTP.
When using NSURLConnection, requests are made in asynchronous form. This mean that you don't wait the end of the request to continue, but you do something else until the NSURLConnection object tell you that some data arrived or the connection started, failed on ended.
When initializing the NSURLConnection object you have to pass the reference of a delegate. This is the object called by NSURLConnection to inform your program about the status on the connection.
This delegate must have to implement the following methods  :
  • connection:didReceiveResponse : called after the connection is made successfully and before receiving any data. Can be called more than one time in case of redirection.
  • connection:didReceiveData : called for each bloc of data.
  • connectionDidFinishLoading : called only one time upon the completion of the request, if no error.
  • connection:didFailWithError : called on error.

The Google Books service
In this example, we will use the Google Book web service. We selected this service mainly for the easy of use and the concise results. We also selected this service because it use JSON to return the datas, and JSON is the best data interchange format for mobile devices like iPhone because it's lightweight and easy to parse.
A typical request to the search function of the Google Book web service is :
https://www.googleapis.com/books/v1/volumes?q=biologie
where searchterms are replaced by the searched keywords. You can see by clicking on the preceding link that the service return an array of items consisting of books description.
We will extract the volumeInfo/title, volumeInfo/description, volumeInfo/imageLinks/smallThumbnail and volumeInfo/previewLink in this example.

Integrating SBJSon
Unfortunately, neither the MacOS X nor the iOS SDK have some JSON parsing API, despite the fact that Apple use JSON intensively in their own applications.
There is several third party API that can easily be found on the Internet, and we selected SBJSon for this example.
You need to download the latest SBJSon distribution, extract it somewhere and import it in your XCode project under a new group :



Writing the Book business class
The Book class represent a book with only the attributes we need in the example: a title, a description, a thumbnail image and a URL to the book page. A Book object will be initialized using a JSON string corresponding to a single item from the Google Book web service.
This is the interface :
#import <Foundation/Foundation.h>

@interface Book : NSObject {
    NSString *title;
    NSString *desc;
    UIImage *smallThumbnail;
    NSURL *url;
}

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *desc;
@property (nonatomic, retain) UIImage *smallThumbnail;
@property (nonatomic, retain) NSURL *url;

-(id) initWithJSONDictionnary:(NSDictionary*)json;

@end


and the implementation :
#import "Book.h"

@implementation Book

@synthesize title, desc, smallThumbnail, url;

-(id) initWithJSONDictionnary:(NSDictionary*)json {
    self = [super init];
    if (self) {
        NSDictionary* volumeInfo = [json objectForKey:@"volumeInfo"];
        self.title = [volumeInfo objectForKey:@"title"];
        self.desc = [volumeInfo objectForKey:@"description"];
     
        NSString* path = [[volumeInfo objectForKey:@"imageLinks"] objectForKey:@"smallThumbnail"];
        UIImage* image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:path]]];
        self.smallThumbnail = image;
        [image release];
     
        self.url = [NSURL URLWithString:[volumeInfo objectForKey:@"previewLink"]];
    }
    return self;
}

-(void) dealloc {
    [url release];
    [desc release];
    [title release];
    [smallThumbnail release];
    [super dealloc];
}

@end







The GoogleBookService class
This class implements the code to query the Google Books web service using HTTPS and translate the JSON resulting datas into Books business objects.
The resulting objects are stored in a NSMutableArray collection of Books, and the search is started by the -performSearch function :
#import <Foundation/Foundation.h>


@interface GoogleBooksService : NSObject {
    NSMutableArray *books;
    NSMutableData *data;
}

@property (nonatomic, retain) NSMutableArray *books;

-(void) performSearch:(NSString*)keyword;

@end


The data instance variable will be used by the same callbacks to store the raw datas received from by the NSURLConnection object.

Implementing the GoogleBookService class
The first thing is of course to write the -dealloc destructor :
-(void) dealloc {
    [data release];
    [books release];
    [super dealloc];
}


The -performSearch allocate the temporary data storage, build the URL using the keyword (note that you can build a better URL by encoding/escaping the keyword, not done here for clarity), build a NSURLRequest object using the URL then execute the request using NSURLConnection :
-(void) performSearch:(NSString*)keyword {
    data = [[NSMutableData alloc] init];


    NSURL *url = [NSURL URLWithString:

                     [NSString stringWithFormat:
                         @"https://www.googleapis.com/books/v1/volumes?q=%@"
                             keyword]];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                                  delegate:self];
    if (!connection) {
        // this is better if you @throw an exception here
        NSLog(@"error while starting the connection");
        [data release];
    }
 
}


The request to the web service will be executed in a background thread by the connection object. This thread will call the callback functions, in order :
connection:didReceiveResponse, after the connection object have received the response and HTTP headers from the remote web server :
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [data setLength:0];
}

connection:didReceiveData, for each block of raw data received :
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)someData {
    [data appendData:someData];
}

connection:didFailWithError, in case of error :
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    [connection release];
    [data release];
}

connectionDidFinishLoading, at the end of the request, if no error have happened. In this function we use SBJSon to translate the raw datas in Books objects :
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [connection release];
    NSString *jsonString = [[NSString alloc] initWithData:data
                                                 encoding:NSUTF8StringEncoding];
    [data release];

    NSArray* jsonItems = [[jsonString JSONValue] objectForKey:@"items"];
    id item;
    books = [[NSMutableArray alloc] init];
    for (item in jsonItems) {
        Book* book = [[Book alloc] initWithJSONDictionnary:item];
        [books addObject:book];
    }
    [jsonString release];
}

2 comments:

John Stewart said...

can you show how to use this created array in your viewcontroller to load array data into ui tableview. i am having a tough time getting this to work with NSURLConnection but had it working using initWithContentsOfUrl but i cant seem to get the poplated array data into my view controller. Thanks!

HM said...

Hi John,
You have to implement an UITableViewController. In the - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method you just have to pick up the element in the array, like that :

Book* book = [service.books objectAtIndex:indexPath.row];
cell.textLabel.text = book.title;
cell.detailTextLabel.text = book.desc;
cell.imageView.image = book.smallThumbnail;