RestKit - CoreData

Episode #52 | 12 minutes | published on February 7, 2013
Subscribers Only
In this episode we continue on our exploration of RestKit, this time with a focus on CoreData. It turns out to be fairly easy to change our existing code to support saving the responses into NSManagedObject classes in a database.

Episode Links

Making our model objects with with Core Data

The first step is to create the CoreData model. Each of the entities match the class names & properties we've already created. In addition, we make sure that we've specified the Class Name explicitly to match our class name we've already created.

Our models currently inherit from NSObject, but we'll need to change that to NSManagedObject to work with CoreData. Next we'll have to specify the property list as @dynamic. Lastly, we'll have to change our collection types to match what we've modeled in CoreData.

#import <CoreData/CoreData.h>

@interface BeerStyle : NSManagedObject

@property (nonatomic, strong) NSNumber *styleId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *styleDescription;
@property (nonatomic, copy) NSString *category;
@property (nonatomic, strong) NSSet *beers;

@end

@implementation BeerStyle
@dynamic styleId, styleDescription, name, category, beers;
@end

#import &lt;CoreData/CoreData.h&gt;

@class BeerStyle, Brewery;

@interface Beer : NSManagedObject

@property (nonatomic, strong) NSString *abv;
@property (nonatomic, strong) NSString *ibu;
@property (nonatomic, strong) NSString *labelIconImageUrl;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSOrderedSet *breweries;
@property (nonatomic, strong) BeerStyle *style;

@property (nonatomic, readonly) NSString *brewery;

@end

@implementation Beer

@dynamic abv, ibu, labelIconImageUrl, name, breweries, style;

- (NSString *)brewery {
    return [[self.breweries firstObject] name];  // had to change this to work with sets
}

@end
#import &lt;CoreData/CoreData.h%gt;

@interface Brewery : NSManagedObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *website;

@end

@implementation Brewery
@dynamic name, website;
@end

Now we have a working model, next we deal with the CoreData setup. For that, we can use RKManagedObjectStore, which can really simplify the entire process.

Setting up CoreData using RKManagedObjectStore

I decided to encapsulate this inside of a class, so I created BeerInfoDataModel.

#import &lt;RestKit/RestKit.h&gt;

@interface BeerInfoDataModel : NSObject

@property (nonatomic, strong) RKManagedObjectStore *objectStore;

+ (id)sharedDataModel;
- (void)setup;

@end

We start off with a simple singleton implementation:

#import "BeerInfoDataModel.h"
#import &lt;CoreData/CoreData.h&gt;

@interface BeerInfoDataModel ()
@end

@implementation BeerInfoDataModel

+ (id)sharedDataModel {
    static BeerInfoDataModel *__sharedDataModel = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        __sharedDataModel = [[BeerInfoDataModel alloc] init];
    });

    return __sharedDataModel;
}

- (void)setup {
}
@end

Next we need to do the work in setup.

- (NSManagedObjectModel *)managedObjectModel {
    return [NSManagedObjectModel mergedModelFromBundles:nil];
}

- (id)optionsForSqliteStore {
    return @{
             NSInferMappingModelAutomaticallyOption: @YES,
             NSMigratePersistentStoresAutomaticallyOption: @YES
            };
}

- (void)setup {
    self.objectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:[self managedObjectModel]];

    NSString *path = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"beerInfo.sqlite"];
    NSLog(@"Setting up store at %@", path);
    [self.objectStore addSQLitePersistentStoreAtPath:path
                              fromSeedDatabaseAtPath:nil
                                   withConfiguration:nil
                                             options:[self optionsForSqliteStore]
                                               error:nil];
    [self.objectStore createManagedObjectContexts];
}

I've extracted out the model into a method in case we need to change it at some point. By default this will find the model from our bundle and return it.

An important step here is the call to createManagedObjectContexts, which is responsible for maintaining your main managed object context (called mainQueueManagedObjectContext).

Next, we turn to our MappingProvider to indicate that the mappings should use entities instead of plain model objects.

Configuring the Mappings for CoreData

We simply need to change how we create the mapping for each method to something like this:

 RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:@"BeerStyle"
                                                   inManagedObjectStore:[[BeerInfoDataModel sharedDataModel] objectStore]];

The rest of the mapping stays the same.

Changing the view controllers

We need to tell the view controllers about our object store & managed object context so that it can save the responses in the database. The changes are shown in bold.

- (void)loadBeerStyles {
    NSIndexSet *statusCodeSet = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful);
    RKMapping *mapping = [MappingProvider beerStyleMapping];
    RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping
                                                                                       pathPattern:@"/v2/styles"
                                                                                           keyPath:@"data"
                                                                                       statusCodes:statusCodeSet];
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://api.brewerydb.com/v2/styles?key=%@",
                                       BREWERY_DB_API_KEY
                                       ]];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    <strong>RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request
                                                                                      responseDescriptors:@[responseDescriptor]];
    RKManagedObjectStore *store = [[BeerInfoDataModel sharedDataModel] objectStore];
    operation.managedObjectCache = store.managedObjectCache;
    operation.managedObjectContext = store.mainQueueManagedObjectContext;</strong>

    [operation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        self.styles = mappingResult.array;
        [self.tableView reloadData];
        [SVProgressHUD dismiss];
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        NSLog(@"ERROR: %@", error);
        NSLog(@"Response: %@", operation.HTTPRequestOperation.responseString);
        [SVProgressHUD showErrorWithStatus:@"Request failed"];
    }];

    [operation start];
}

An identical change needs to happen in BeersViewController as well.

Trying it out

After running the application, we notice that it works the same as before, however this time the data has been saved to a sqlite database. We look at the path that was logged out so we can easily navigate there in the Terminal, and inspect the contents with sqlite3.

$ sqlite3 beerInfo.sqlite

> SELECT COUNT(*) FROM ZBEER;
50
> .exit

And that's it! RestKit is now saving responses to the database.

blog comments powered by Disqus