An overview of RestKit, a Core Data enabled iOS and MacOSX framework for Restful applications

le 22/12/2011 par Rémy Saissy
Tags: Software Engineering

ASIHttpRequest, AFNetworking, MKNetworkKit,… The iOS/MacOSX programming landscape is full of helper libraries to deal with asynchronous network communications in your applications.

RestKit can be seen as yet another library to do it. However, its approach is radically different since it does not only address asynchronous networking but also object mapping and a seamless persistance of such mappings locally through CoreData.

This approach is quite interesting when it comes to interact with Restful web services because of the time (and code!) you can save.

In this article, we are going to look at RestKit's main functionalities through the code to do it. The objective is to give you a precise idea of what can be done and how to do it.

Network communications through RKClient

In RestKit, network communications are managed by the RKClient class. This class encapsulates details on the HTTP level and provides control to the developer over it by simply editing properties.

RKClient is where you can find the functionalities of ASIHttpRequest, AFNetworking or MKNetworkKit.

From RestKit standpoint, it is an intermediate layer as we will see later in this article.

RKClient *client = [RKClient clientWithBaseURL:@"http://api.domain.com/v1"];
[RKClient setSharedClient:client];
NSLog(@"This is my default client: %@.", [RKClient sharedClient]);
if ([[RKClient sharedClient] isNetworkReachable]){
    // Do some work...
}

This code creates a new RKClient instance and set it as the default one. You can create as many instances as you want. Each instance is tied to a specific base URL which serves as the prefix to all network communications. RKClient exposes several properties to configure HTTP communications. Here is a review of the most common use case you might have.

Customize HTTP Headers

NSLog(@"HTTP headers added: %@.", [RKClient sharedClient].HTTPHeaders);
[[RKClient sharedClient].HTTPHeaders setObject:@"MyAppSpecialHeader" forKey:@"X-MyApp-Special-Header"];
[[RKClient sharedClient].HTTPHeaders removeObjectForKey:@"X-Old-Key"];

This allows you to control custom HTTP headers you want to use with your requests through this client.

SSL communications

RKClient *sslClient = [RKClient clientWithBaseURL:@"https://api.domain.com/v1"];
sslClient.disableCertificateValidation = YES;
SecCertificateRef myCustomRootCertRef = SecCertificateCreateWithData(NULL, certData);
[sslClient addRootCertificate:myCustomRootCertRef];
NSLog(@"NSSet of my additional root certs: %@.", sslClient.additionalRootCertificates);

As shown in the example, you can disable SSL certificate validation and add your own root certificates for a given RKClient.

Request queue and Cache policy control

RKClient is asynchronous, its concurrency level is customizable and requests can suspended in which case they are enqueued until the queuing limit is reached.

[RKClient sharedClient].requestQueue.concurrentRequestsLimit = 2;
[RKClient sharedClient].requestQueue.requestTimeout = 10;
[RKClient sharedClient].suspended  = YES;
NSLog(@"Number of requests in the queue: %d. Total number of request currently loading: %d.",
[RKClient sharedClient].loadingCount, [RKClient sharedClient].count);

It is also possible to access and control the cache of the RKClient instance.

Several policies such as use the cache only when offline or use the cache when the request timeouts as builtin. The developer can dynamically change the caching policy and also has a direct access to the cache implementation so she can invalidate entries, add others, ...

[RKClient sharedClient].cachePolicy = RKRequestCachePolicyLoadIfOffline|RKRequestCachePolicyTimeout;
[RKClient sharedClient].requestCache.storagePolicy = RKRequestCacheStoragePolicyPermanently;

Builtin support for authentication schemes

RestKit natively supports 4 authentication schemes:

  • HTTP Auth
  • HTTP Basic
  • OAuth1
  • OAuth2

This is configured through properties of RKClient. For example, to configure OAuth2 you would do the following.

[RKClient sharedClient].authenticationType = RKRequestAuthenticationTypeOAuth2;
[RKClient sharedClient].OAuth2AccessToken = @"YourOAuth2AccessTokenHere";
[RKClient sharedClient].OAuth2RefreshToken = @"YourOAuth2RefreshTokenHere";

Once configured this way, all network communications will use the credentials you specified. Other authentication schemes are configured the same way. Each have its own set of properties in RKClient.

A real life example with RKClient

Now let's consider a real life example. Let's say that we want to retrieve an RSS feed of today from a website which requires an HTTP Basic authentication. We don't want to cache anything and we also don't want to wait for more than 10 seconds to retrieve the feed. The code to do it would look like this:

#import <Foundation/Foundation.h>
#import <RestKit/RestKit.h>

@interface MyClass : NSObject<RKRequestDelegate>

- (void)retrieveFeed;

@end

@implementation MyClass

- (void)retrieveFeed
{
  RKClient *client = [RKClient clientWithBaseURL:@"http://rss.domain.com"];
  client.requestQueue.requestTimeout = 10;
  client.cachePolicy = RKRequestCachePolicyNone;
  client.authenticationType = RKRequestAuthenticationTypeHTTPBasic;
  client.username = @"rssuser";
  client.password = @"rsspwd";
  NSDictionary *queryParameters = [NSDictionary dictionaryWithObjectsAndKeys:@"today", @"date", nil];
  NSString *getResourcePath = RKPathAppendQueryParams(@"/feed.xml", queryParameters);
  [client get:getResourcePath delegate:self];
}

- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response
{
  if (request.method == RKRequestMethodGET) {
        id xmlParser = [[RKParserRegistry sharedRegistry] parserForMIMEType:RKMIMETypeXML];
        NSError *error = nil;
        id parsedResponse = [xmlParser objectFromString:[response bodyAsString] error:&error];
        if (error == nil)
            NSLog(@"GET:/user returned with HTTP Code %d and parsedContent: %@", [response statusCode], parsedResponse);
  }
}

- (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error
{
  if (request.method == RKRequestMethodGET) {
    NSLog(@"Failure of GET:/user with error %@.", error);
  }
}
@end

As you have probably noticed, a delegate is provided to handle the asynchronous results. RestKit relies a lot on delegate both internally and for its public interfaces.

Another interesting point is how we parsed the response of the feed which is expected to be XML content.

Indeed, RestKit provides a parser registry that you can use to get an instance of a parser for one of these four mimetypes:

  • RKMIMETypeJSON for application/json
  • RKMIMETypeFormURLEncoded for application/x-www-form-urlencoded
  • RKMIMETypeXML for application/xml
  • RKMIMETypeTextXML for text/xml

JSON parsing is based on JSonKit and XML parsing is based on libxml2. Therefore, most of the time, you don't need to include another third party library for parsing network responses.

Object Mapping through RKObjectManager

What makes RestKit quite different from other libraries is the object mapping layer and its seamless support for persistence through CoreData.

RKObjectManager is the entry point in RestKit to manage object mappings. Each instance of this class encapsulates an RKClient, an RKObjectRouter, an RKObjectMappingProvider and an RKManagedObjectStore. But let's see by the examples how to use the Object Manager.

Create a mapper for a remote API

RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:@"http://api.domain.com/v1"];
RKObjectManager *sslObjectManager = [RKObjectManager objecxtManagerWithBaseURL:@"https://api.domain.com/v1"];

// Initialize the managed object store because both will have Core Data managed objects.
objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"api.sql"]; 
sslObjectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"apissl.sql"]; 

// We want the SSL Object Manager to be our sharedManager.
[RKObjectManager setSharedManager:sslObjectManager];

You can have several object managers and choose which one is the sharedManager. Each  Object Manager instance has an RKClient  instance. Therefore you can control RKClient parameters on a per remote API mapper basis.

Create a mapping and register a route for it

RKManagedObjectMapping *myMapping = [RKManagedObjectMapping mappingForClass:[MyClass class]];

// Map attributes
[myMapping mapAttributes:@"id", @"name", nil];

// These attributes have different name in objc and JSON.
[myMapping mapKeyPathsToAttributes:@"expirationTime", @"expiration_time", nil];

// Since it is a Core Data managed class, we can indicate to RestKit which property is the primary key (optional).
[myMapping setPrimaryKeyAttribute:@"id"];

// Set relationships. Previously mapped classes are used here.
[myMapping hasOne:@"user" withMapping:[[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:@"userMapping"]];
[myMapping hasMany:@"sessions" withMapping:[[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:@"sessionMapping"]];

// Set both the mapping and the serialization mapping.
[[RKObjectManager sharedManager].mappingProvider setMapping:myMapping forKeyPath:@"tokenMapping"];
[[RKObjectManager sharedManager].mappingProvider setSerializationMapping:[myMapping inverseMapping]  forClass:[MyClass class]];

// Register two routes for this class, a GET and a POST.
[[RKObjectManager sharedManager].router routeClass:[MyClass class] toResourcePath:@"/api/foo" forMethod:RKRequestMethodGET];

// The POST route has a dynamic parameter which is the objc property name prefixed by a semicolon.
[[RKObjectManager sharedManager].router routeClass:[MyClass class] toResourcePath:@"/api/foo/:id" forMethod:RKRequestMethodPOST];

Requesting a resource using its route

// Our object is managed through CoreData. RestKit takes care of updating it.
@interface MyClass : NSManagedObject

@property (retain, nonatomic) NSInteger id;
@property (retain, nonatomic) NSString *name;

@end

@implementation MyClass

@dynamic id, name;

@end

@interface MyController : UIViewController<RKObjectLoaderDelegate>

- (void)loadData;

@end

@implementation MyController

- (void)loadData
{
  // Retrieve an instance stored in CoreData. RestKit provides a category to encapsulate Core Data interactions.
  MyClass *obj = [MyClass findFirstByAttribute:@"id" withValue:@"42"];
  obj.name = @"A new name to set remotely";

  // Perform the post request.
  [MyClass postObject:obj delegate:self];
}

- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObject:(id)object
{
  NSLog(@"Request succeed and  response loaded into CoreData: %@.", object);
}

- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error
{
  NSLog(@"An error occurred: %@.", error);
}
@end

Other response delegates are available and let the developer receive its content as a dictionary or an array of objects if the return was expected to be an array.

Requesting a resource without a route

Loading a request without a route is also possible. The use case is when a request expects an array of a specific object. In order to request without a route, the developer uses the loadObjectsAtResourcePath: method call.

RKObjectMapping *userMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForKeyPath:@"sessionMapping"];

[[RKObjectManager sharedManager] loadObjectsAtResourcePath:@"/api/foo" objectMapping:userMapping delegate:self];

Nothing else changes.

Configuring a mapping

In order to cope with a maximum of situation, RestKit provides a set of properties to configure each mapping to fit to what the remote API returns or expects to receive. Here are some of the most useful of these properties:

Handle missing attributes
myMapping.setDefaultValueForMissingAttributes = YES;
myMapping.setNilForMissingRelationships = YES;

You can set both missing attributes and relationships to nil.

Mapping unpredictable JSON keys

Let's say for example that user id/name is the key in a JSON and that for each "key", the value is the user object. Let's consider the following JSON:

{ "users":
  {
    "foo": { "id": 1234, "email": "foo@domain.com" },
    "bar": { "id": 5678", "email": "bar@domain.com" }
  }
}

To handle this kind of unpredictable mapping in RestKit, we would do:

RKObjectMapping *myMapping = [RKObjectMapping mappingForClass:[User class]];
// RestKit cannot infer this is a collection, so we force it
myMapping.forceCollectionMapping = YES;
// Map our attributes.
[myMapping mapKeyOfNestedDictionaryToAttribute:@"firstName"];
[myMapping mapFromKeyPath:@"(firstName).id" toAttribute:"userID"];
[myMapping mapFromKeyPath:@"(firstName).email" toAttribute:"email"];

[[RKObjectManager sharedManager].mappingProvider setObjectMapping:myMapping forKeyPath:@"users"];
Dates are formatted in a very specific way

RestKit let the developer handle it in different ways:

  • By default, a mapping uses an array of date formatters
  • Each mapping can define its own array of data formatters to use instead of the global one
  • Each mapping can define its preferred date formatter to use in first place

myMapping.preferredDateFormatter = [[[NSDateFormatter alloc] init] autorelease];
myMapping.dateFormatters  = [NSArray arrayWithObjects:[[[NSDateFormatter alloc] init] autorelease], [[[NSDateFormatter alloc] init] autorelease], [[[NSDateFormatter alloc] init] autorelease], nil];

// Set as the application wide default set of formatters.
[RKObjectMapping setDefaultDateFormatters:myMapping.dateFormatters];
Avoids duplicate entries of an object in the Core Data store
RKManagedObjectMapping *myMapping = [RKManagedObjectMapping mappingForClass:[CoreDataManagerObject class]];
myMapping.primaryKeyAttribute = @"id";

Note that this is specific to objects managed with CoreData.

Initializing using a seed database

You can initialize the Core Data store using a seed database. For example you may want to seed your store if it is empty.

RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:@"http://api.domain.com/api"];

// ... Initializing mappings.
NSArray *objectsInStore = [MyObject allObjects];
if (objectsInStore count] == 0) {
  NSString *storeFilename = objectManager.objectStore.storeFilename;
  NSString *pathToSeedDB = [[NSBundle mainBundle] pathForResource:@"MySeedDB" ofType:@".db"];
  RKManagedObjectStore *objectStore = objectStoreWithStoreFilename:storeFilename usingSeedDatabaseName:pathToSeedDB managedObjectModel:nil delegate:self];
  objectManager.objectStore = objectStore;
}

Handling Core Data errors

Sometimes the Core Data store may have failure. For example when your data model is invalid. RestKit provides a way to handle such events with the RKManagedObjectStoreDelegate.

@interface MyAppDelegate : UIResponder <RKManagedObjectStoreDelegate>
@end

@implementation MyAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  RKObjectManager *objectManager = [RKObjectManager   objectManagerWithBaseURL:@"http://api.domain.com/api"];
  objectManager.objectStore.delegate = self;
}

// Delegates

- (void)managedObjectStore:(RKManagedObjectStore *)objectStore didFailToCreatePersistentStoreCoordinatorWithError:(NSError *)error
{
  NSLog(@"Fail to create a persistent store. Error: %@", error);
}

- (void)managedObjectStore:(RKManagedObjectStore *)objectStore didFailToDeletePersistentStore:(NSString *)pathToStoreFile error:(NSError *)error;
{
  NSLog(@"Fail to delete a persistent store at path %@. Error: %@", pathToStoreFile, error);
}

- (void)managedObjectStore:(RKManagedObjectStore *)objectStore didFailToCopySeedDatabase:(NSString *)seedDatabase error:(NSError *)error
{
  NSLog(@"Fail to copy seed database: %@. Error: %@", seedDatabase, error);
}

- (void)managedObjectStore:(RKManagedObjectStore *)objectStore didFailToSaveContext:(NSManagedObjectContext *)context error:(NSError *)error exception:(NSException *)exception
{
  NSLog(@"Fail to save context: %@. Error: %@ (%@)", context, error, exception);
}
@end

Unit Testing

In RestKit, all network communications are done using two classes: RKRequest and RKResponse.

Unit testing thus requires reimplementing the - (void)fireASynchronousRequest in RKRequest to avoid enqueuing the request and mocking your API.

In this case, your reimplemented method will build its own RKResponse packet and call the relevant delegates.

To know which one to use, it is needed to look at the source code of the version of RestKit compiled with the application.

Here is an example of fireAsynchronousRequest modification for unit test.

- (void)fireASynchronousRequest
{
// From the original implementation.

[self prepareURLRequest];
NSString* body = [[NSString alloc] initWithData:[_URLRequest HTTPBody] encoding:NSUTF8StringEncoding];
NSLog(@"Sending %@ request to URL %@. HTTP Body: %@", [self HTTPMethod], [[self URL] absoluteString], body);
[body release]; 

_isLoading = YES;
if ([self.delegate respondsToSelector:@selector(requestDidStartLoad:)])
[self.delegate requestDidStartLoad:self]; 

RKResponse* response = [[[RKResponse alloc] initWithRequest:self] autorelease];
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil];

// Now execute the mocking code.
NSString *bundlePath = nil;
NSInteger statusCode = 200;

id requestJsonObject = nil;
if (self.params != nil) {

  NSString *jsonString = [[[NSString alloc] initWithData:[self.params HTTPBody] encoding:NSUTF8StringEncoding] autorelease];

  // If you use the latest version of RestKit with Object Mapper 2.0:
  id parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:RKMIMETypeJSON];
  NSAssert1(parser, @"Cannot perform object load without a parser for MIME Type '%@'", RKMIMETypeJSON);
  NSError **error = nil;
  requestJsonObject = [parser objectFromString:jsonString error:error];
}

// Switch to determine which to to use
…

// Prepare the results.
NSData *responseData = nil;
if (bundlePath == nil)
responseData = [NSData data];
else
{
// Load the mock file.
NSString *responseString = [NSString stringWithContentsOfFile:bundlePath encoding:NSUTF8StringEncoding error:nil];
responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
}

// Send the response. This is a superclass to be able to return a custom statusCode.
MyHTTPURLResponse *urlResponse = [[[MyHTTPURLResponse alloc] initWithURL:_URL MIMEType:RKMIMETypeJSON expectedContentLength:[responseData length] textEncodingName:nil] autorelease];
[urlResponse setStatusCode:statusCode];

// Delegates in the order RestKit is expecting it.
[response connection:nil didReceiveResponse:urlResponse];
[response connection:nil didReceiveData:responseData];
if ([response isError] == YES)
[response connection:nil didFailWithError:[NSError errorWithDomain:@"test domain" code:statusCode userInfo:nil]];
else
[response connectionDidFinishLoading:nil];
}

Conclusion

The choice of integrating a third party library is an important one. RestKit is an active project and from my experience, the questions you might ask on their forums will find an answer quickly.

Moreover, if you need object mapping or Core Data support, RestKit is definitely a very good choice since its clean architecture and API saves a lot of time to developers.

If you don't need such functionalities, then you should focus on RKClient's functionalities compared to other frameworks and keep in mind that even though it is quickly moving towards blocks, RestKit still heavily relies on delegates.

Feel free to let me know your feelings and comments.

References

RestKit: http://restkit.org/

RestKit Source Code on gitHub: https://github.com/RestKit/RestKit

RestKit Google Group: https://groups.google.com/forum/#!forum/restkit