Note: The post is quite outdated. Please use the manual on the official RestKit project website.

In this short post I’ll show how I use RestKit library in my iOS projects.

First of all, I want to say that for managing all my libraries in iOS projects I use CocoaPods (kind of Gemfile bundler, but for iOS), which I strongly recommend using.

So, RestKit… open up your Podfile and add

dependency 'RestKit/Network'
dependency 'RestKit/UI'
dependency 'RestKit/ObjectMapping'
dependency 'RestKit/ObjectMapping/CoreData'
dependency 'RestKit/ObjectMapping/JSON'

I also use Injective dependency a lot, so I add dependency 'Injective' to my Podfile

Next, run pod install which will download latest RestKit (In my case - version 0.10.0) and integrate it to your project.

The code

Examples I’ll provide will use RM class prefix, and API I’ll refer is from my start-up rabat.me. First I’ll write examples without data persistancy, than I’ll provide examples how to use CoreData. Also, I add #import <RestKit/RestKit.h> and #import "IJContext.h" to my App-Prefix.pch

You can find full source code of the project on GitHub https://github.com/xslim/RabatMe

Initialization

For initializing Injective library, edit your AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [IJContext setDefaultContext:[[IJContext alloc] init]];

What I also like doing, is initializing RestKit-s logging, so I can use it in my app and changing verbocity level from enviroment variables.

// This goes to application:didFinishLaunchingWithOptions:
RKLogInitialize();
RKLogConfigureFromEnvironment();

Create class RMApiConnector which we will use for all our RestKit bootstrapping and network connections.

NSString * const RMErrorDomain = @"org.API.ErrorDomain";

@interface RMApiConnector ()
- (void)fireErrorBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock onErrorInResponse:(RKResponse *)response;
@end

@implementation RMApiConnector

injective_register_singleton(RMApiConnector)

- (id)init {
    self = [super init];
    if (self) {
        [self setupConnector];
        [self setupMapping];
    }
    return self;
}

injective_register_singleton will add RMApiConnector to singleton collection on DI, so we will be able to access it like

RMApiConnector *connector = [RMApiConnector injectiveInstantiate]

Setting up Object Loader

- (void)setupConnector {
    NSString *baseUrl = @"http://example.org/api/"
    RKObjectManager *manager = [RKObjectManager sharedManager];

    if (!manager) {
        manager = [RKObjectManager objectManagerWithBaseURL:[RKURL URLWithString:baseUrl]];
        manager.client.serviceUnavailableAlertEnabled = YES;
        manager.requestQueue.showsNetworkActivityIndicatorWhenBusy = YES;
    } else {
        manager.client.baseURL = [RKURL URLWithString:baseUrl];
    }
}

Setting up Object Mapping

- (void)setupMapping {
    RKObjectMappingProvider *omp = [RKObjectManager sharedManager].mappingProvider;

    RKObjectMapping *productMapping = [RMProduct mapping];
    [omp addObjectMapping:productMapping];
    [omp setObjectMapping:productMapping forResourcePathPattern:@"/products"];

    RKObjectMapping *shopMapping = [RMShop mapping];
    [omp addObjectMapping:shopMapping];
    [omp setObjectMapping:shopMapping forResourcePathPattern:@"/shops/:id"];
    [omp setObjectMapping:shopMapping forResourcePathPattern:@"/shops/near/:lat/:lng/:radius"];
}

The mapping itself I store with Models like

@class RKObjectMapping;

@interface RMShop : NSObject

@property (nonatomic, strong) NSString *itemId;
@property (nonatomic, strong) NSNumber *points;
@property (nonatomic, strong) NSString *name;
// Some other properties

@property (nonatomic, strong) NSArray *products;
@property (nonatomic, strong) NSArray *rewards;

+ (RKObjectMapping *)mapping;

@end
@implementation RMShop

@synthesize itemId, points; // and others

+ (RKObjectMapping *)mapping {
    RKObjectMapping *objectMapping = [RKObjectMapping mappingForClass:[self class] usingBlock:^(RKObjectMapping *mapping) {
        [mapping mapAttributes:@"name", @"address", @"distance", @"location", @"points", nil];
        [mapping mapKeyPathsToAttributes:
         @"id", @"itemId",
         @"description", @"descriptionText",
         @"distance_unit", @"distanceUnit",
         nil];
    }];
    [objectMapping hasMany:@"products" withMapping:[RMProduct mapping]];
    [objectMapping hasMany:@"rewards" withMapping:[RMReward mapping]];
    return objectMapping;
}

@end

Creating API requests

Let’s create few requests

- (void)loadNearbyShopsForLocation:(CLLocation *)location onLoad:(RKObjectLoaderDidLoadObjectsBlock)loadBlock onError:(RKRequestDidFailLoadWithErrorBlock)failBlock
{
    NSString *params = [NSDictionary dictionaryWithKeysAndObjects:
                        @"lat", [NSNumber numberWithDouble:location.coordinate.latitude],
                        @"lng", [NSNumber numberWithDouble:location.coordinate.longitude],
                        @"radius", [NSNumber numberWithInt:20], // later change this to accuracy
                        nil];

    NSString *url = [@"/shops/near/:lat/:lng/:radius" interpolateWithObject:params];

    RKObjectManager *manager = [RKObjectManager sharedManager];
    [manager loadObjectsAtResourcePath:url usingBlock:^(RKObjectLoader *loader) {
        loader.onDidLoadObjects = loadBlock;
        loader.onDidFailWithError = failBlock;
        loader.onDidFailLoadWithError = failBlock;
        loader.onDidLoadResponse = ^(RKResponse *response) {
            [self fireErrorBlock:failBlock onErrorInResponse:response];
        };
    }];
}

What we can see here, is that I’m using block-based API of RestKit. loadBlock will get array of loaded objects. failBlock will get NSError object on error. Also I’m using fireErrorBlock:onErrorInResponse: helper to get error message from my API

- (void)fireErrorBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock onErrorInResponse:(RKResponse *)response {

    if (![response isOK]) {
        id parsedResponse = [response parsedBody:NULL];
        NSString *errorText = nil;
        if ([parsedResponse isKindOfClass:[NSDictionary class]]) {
            errorText = [parsedResponse objectForKey:@"error"];
        }
        if (errorText) failBlock([self errorWithMessage:errorText code:[response statusCode]]);
    } else {
        //id parsedResponse = [response parsedBody:NULL];
        //RKLogTrace(@"response: [%@] %@\n%@", [parsedResponse class], parsedResponse, [response bodyAsString]);
    }
}

What if we want to make a POST call? Easy. Just tell the loader loader.method = RKRequestMethodPOST; and pass NSDictionary POST params to loader.params

RKObjectManager *manager = [RKObjectManager sharedManager];
[manager loadObjectsAtResourcePath:@"/stamps" usingBlock:^(RKObjectLoader *loader) {
    loader.method = RKRequestMethodPOST;
    loader.params = [NSDictionary dictionaryWithKeysAndObjects:
                         @"code", clientCode,
                         @"products", productsToSend,
                         nil];

What if we don’t need to load any objects? Let’s say we want to make client Authentication.

- (void)authenticateWithLogin:(NSString *)login password:(NSString *)password onLoad:(RKRequestDidLoadResponseBlock)loadBlock onFail:(RKRequestDidFailLoadWithErrorBlock)failBlock
{
    [[RKClient sharedClient] post:@"/login" usingBlock:^(RKRequest *request) {
        request.params = [NSDictionary dictionaryWithKeysAndObjects:
                          @"employee[email]", login,
                          @"employee[password]", password,
                          nil];
        request.onDidLoadResponse = ^(RKResponse *response) {

            id parsedResponse = [response parsedBody:NULL];
            NSString *token = [parsedResponse valueForKey:@"authentication_token"];
            //NSLog(@"response: [%@] %@", [parsedResponse class], parsedResponse);

            if (token.length > 0) {
                NSLog(@"response status: %d, token: %@", response.statusCode, token);
                [[RKClient sharedClient] setValue:token forHTTPHeaderField:@"X-Rabatme-Auth-Token"];

                if (loadBlock) loadBlock(response);
            }

            [self fireErrorBlock:failBlock onErrorInResponse:response];
        };
        request.onDidFailLoadWithError = failBlock;
    }];
}

Integrating CoreData

Great. What about CoreData? Change RMApiConnector’s init method

    if (self) {
        [self setupConnector];
        [self setupDB];
        [self setupMapping];
    }
- (void)setupDB {
    RKObjectManager *manager = [RKObjectManager sharedManager];
    manager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"HotSpots.sqlite"];
}

For mapping Managed Objects you do

- (void)setupMapping {
    RKObjectManager *manager = [RKObjectManager sharedManager];
    RKObjectMappingProvider *omp = [RKObjectManager sharedManager].mappingProvider;

    RKManagedObjectMapping *hotSpotMapping = [RKManagedObjectMapping mappingForClass:[HotSpot class] inManagedObjectStore:manager.objectStore];
    [hotSpotMapping mapAttributes:@"name", @"city", @"country", @"latitude", @"longitude", @"type", @"zipcode", nil];
    [hotSpotMapping mapKeyPathsToAttributes:
     @"address", @"street",
     @"description", @"details",
     @"id", @"hotSpotID",
     @"openinghours", @"openingHours",
     nil];
    hotSpotMapping.primaryKeyAttribute = @"hotSpotID";

    [omp addObjectMapping:hotSpotMapping];
    [omp setObjectMapping:hotSpotMapping forResourcePathPattern:@"app/location"];

Now if Object Mapper will see that the object he’s mapping is a NSManagedObject he will check the DB if the’r already object with same primaryId. If yes - he’ll update it with new values, if no - he’ll create one for you.