Parsing JSON and Object Mapping with RestKit
So now that I’ve got the basics of getting data, what’s a guy to do? Parse it baby! I’m going to be sticking with some pretty basic tasks around store and product location searches. I’m also going to stick with restkit.org since things are going pretty well with this.
What’s Life Without a Roadblock (or two)?
Well without challenges life would be just too easy. Straight off I thought I lost my brain. Repeating the installation tasks for restkit into my project wasn’t working. I would drag over the project file and nothing. It would be added and then – nuttin. After repeating the steps, re-reading all the instructions, and banging my head on the table I figured out that its good to read those little red error thingies. Yeah – you can’t include the same project in two active xcode projects (open at the same time).
Copy the project, re-peat the steps and good to go….or not. I was getting different linker errors and such now. The best approach is really to get a clean gitmodule for each project following very carefully the instructions. I also noticed that a new rev appeared while I wasn’t watching.
I see also now that there’s a nice new “maven” type concept for adding dependencies to a project…cocoapods.org – cool! I’;ll have to try that at some point.
Object Mapping with RestKit
So now back to what I wanted to do – figure out how to map data to classes and parse out my JSON. This is the job of the RKObjectManager class and other related classes as introduced in the RestKit wiki here. I’m going to stick with my basic API calls for store locations – so I’ll need some classes to model store locations and such. I’ll start with this as a basic class for store location:
#import <Foundation/Foundation.h> @interface StoreLocation : NSObject @property (nonatomic, retain) NSString *storeName; @property (nonatomic, retain) NSString *address; @property (nonatomic, retain) NSString *city; @property (nonatomic, retain) NSString *state; @property (nonatomic, retain) NSString *zip; @property (nonatomic, retain) NSString *phone1; @property (nonatomic, retain) NSString *phone2; @property (nonatomic, retain) NSString *distance; @end
That should be good enough for now. I’m also going to stick with my singleton for managing the RKClient instance for my app.
With these things added my project now looks like the image to the right. A quick build and…failure. RKClient cannot be found? WTF? After some more head banging, cursing, etc. I figure out that all the tutorial examples are actually supplied with and built from the 0.10 branch of restkit and the 0.20 (now master) has eliminated the RKClient class in favour of AFNetworking. Ummm…well ok. This leaves me wondering how good any of the docs are. Not good. After some flailing around I’ve figured out that RKManager has as a property the HTTPClient from AFNetworking and this can be initialized with the following line:
RKObjectManager *manager = [RKObjectManager managerWithBaseURL: [NSURL URLWithString:[HOBSDKManager shared].hostName]];
The RKTwitter example also shows basic means for how the 0.20-pre5 release works. I’m going to study this deeply to better know how to modify my core manager class….ok one day later things are looking pretty good. As before my AppDelegate sends a message to my HOBSDKManager to startUp. The HOBSDKManager class centralizes things like URLs, hostnames, and other parameters used across the app: Here’s the code:
+ (void) startUp { [HOBSDKManager shared].appId = @"asau87sas6765rsashggs5565"; [HOBSDKManager shared].hostName = @"http://www.removedforyourprotection.net"; //Setup some logging for network and object mapping issues. RKLogConfigureByName("RestKit/Network", RKLogLevelDebug); RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace); //Setup the HTTP client [AFNetworkActivityIndicatorManager sharedManager].enabled = YES; NSURL *baseURL = [NSURL URLWithString:[HOBSDKManager shared].hostName]; AFHTTPClient* client = [[AFHTTPClient alloc] initWithBaseURL:baseURL]; [client setDefaultHeader:@"Accept" value:RKMIMETypeJSON]; [HOBSDKManager shared].service= @"StoreLocator_Search"; // Initialize RestKit RKObjectManager *objectManager = [[RKObjectManager alloc] initWithHTTPClient:client]; RKLogInfo(@"Configured RestKit Manager: %@", objectManager); }
Pretty straightforward – just different than in my previous post. With that setup then in my AppDelegate class I can add the mappings between the JSON response data and my StoreLocation class in didFinishLoadingWithOptions:
RKObjectMapping *storeLocationMapping = [RKObjectMapping mappingForClass:[StoreLocation class]]; [storeLocationMapping addAttributeMappingsFromDictionary:@{ @"StoreName" : @"storeName", @"Address": @"address", @"City": @"city", @"State": @"state", @"Zip": @"zip", @"Phone1": @"phone1", @"Phone2": @"phone2", @"Distance": @"distance" }];
In that code the keys from the class go on the right of the key pair – the JSON keynames on the left. The next step is to setup a response descriptor that can be added to the sharedManager (RKObjectManager that I created earlier). What this will do is tell the ObjectManager what part of the JSON data is the array of store locations for mapping. To the left is my JSON stream and to the right is the descriptor mapper:
{ "Status": "", "Query": "", "MetaData": { "pageNumber": 1, "totalCount": 43, "pageSize": 10 }, "StoreList": [ { "PostalCode": "19050", "Phone1": "(610) 623-9223", "Distance": 1.41, "State": "PA", "Address": "300 E BALTIMORE AVE", "City": "LANSDOWNE", "Phone2": 0, "StoreName": "GIANT" }, { "PostalCode": "19050", "Phone1": "(610) 622-3795", "Distance": 1.49, "State": "PA", "Address": "950 ", "City": "YEADON", "Phone2": 0, "StoreName": "RITE AID PHARMACY" }, { "PostalCode": "19096", "Phone1": "(215) 649-4034", "Distance": 2.28, "State": "PA", "Address": "250 E LANCASTER AVE", "City": "WYNNEWOOD", "Phone2": 0, "StoreName": "SUPER FRESH" }], "IRIStatus": "IRI-UP" } |
// Register our mappings with the provider // using a response descriptor RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: storeLocationMapping pathPattern:nil keyPath:@"StoreList" statusCodes:[NSIndexSet indexSetWithIndex:200]]; [[RKObjectManager sharedManager] addResponseDescriptor:responseDescriptor]; |
So now I am ready to call an API, get some data, and have restkit then map that data into an array of my classes for me to use. I created a very simple GUI with one view controller some fields to enter some criteria for a search and then a UITableView to hold the response data. Barebones to say the least. All that is basic iOS stuff so I’ll skip over all that. In my view controller viewLoadLoad sets up some of the URL for the query, but most of the real work happens in (IBAction) sendRequest: (id) sender. The is called when I click a button in the View. Here’s the method:
- (IBAction) sendRequest: (id) sender { params = [[NSMutableDictionary alloc] init]; [params setObject:productCode.text forKey:@"productcode"]; [params setObject:zipCode.text forKey:@"zipcode"]; RKObjectManager *objectManager = [RKObjectManager sharedManager]; [objectManager getObjectsAtPath:requestUrl parameters:params success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { NSArray* storeLocations = [mappingResult array]; _storeLocations = storeLocations; if(self.isViewLoaded) [responseDataView reloadData]; } failure:^(RKObjectRequestOperation *operation, NSError *error) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:[rror localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; NSLog(@"Hit error: %@", error); }]; }
Above where it says message: [rror localizedDescription] that should of course say “error”. Apparently this is also some kind of commnd in a browser and would generate a funny red box until I removed the “e”.
My GUI has a couple of parameters for zipcode and a product code. I setup these in an NSDictionary and pass that along with the URL into the getObjectAtPath function to get the data. When the data is returned successfully the mapping kicks in, the JSON is parsed out into an array of objects that is assigned to _storeLocations. Finally I to the controller to reloadData if it is loaded. I gotta say – that is pretty darn cool!
Conclusions – RestKit is cool!
With a couple of days of work (with a decent head cold too) I was able to get a pretty complicated set of logic working with relatively little effort. I leaned heavily on some of the examples – especially RKTwitter – kudos to the folks working on restkit that the examples are good!
Some next steps for me are:
- Learn how to post data and then get return data using HTTP POST
- Model some more complex data with relationships
- Learn how to GET/POST attachments like files or images
- Look into the Core Data integration with RestKit
- Update my main SDKManager class to contain an array of service endpoints that are selectable in the UI and will also cause changes in the views and forms you see and use.
Some good stuff here Beren. Will you post about the POST?
Huan
Hey there Huan – long time…its in the oven…maybe tomorrow I’ll have another post.