iOS Core Data Prepopulation

» 26 Mar 2013 »

When developing a mobile app, it’s necessary to ship the app with some default data. I recently came across the task of pre-populating an application with a fair amount of data and found documentation on exactly how to do this with Core Data lacking. Maybe it’s because I’m not a seasoned Core Data expert, but come on! Searching The Google also turned up many others having a similar hard time finding clarity on this issue.

This article presents a method for pre-populating a Core Data application on IOS.

Procedure

We’ll walk through the specific steps in detail, but this is the general approach to Core Data pre-population:

  1. Generate a Core Data database with pre-populated data
  2. Add this database to the project
  3. When app starts for the first time, copy the pre-populated DB into the managed store and start application.
  4. Profit!

Step 1: Generate a Core Data database

A common misconception about Core Data is that ‘its just a sqlite database, gosh! ’ Which is true; however sqlite is just one of the ways Core Data can persist itself. The problem with this conception is the Core Data sqlite schemea is proprietary - don’t even try opening it up with any tools because the data and column names won’t make any sense. Changing anything may render the DB unusable. What we need to do is somehow populate the DB via the Core Data interface. This may require writing our own parsing code, or using an app like Core Data Editor

In apps I’ve done, data was first extracted as JSON, manually parsed, and then saved within Core Data.

Step 2: Add the pre-populated database to the project

Add the function we’ve created to parse and prepopulate Core Data somewhere within the Appdelegate, or another place to be executed on launch. Next, within the AppDelegate, we need to find the location of our newly populated sqlite file. Add this code to the ‘persistentStoreCoordinator’ in AppDelegate :

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    /* ... */    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Foobar.sqlite"];
    NSLog(@"CoreData Location: %@", storeURL);

Awesome, now we gotcha! Rename this file to something like ‘Prepopulated.sqlite’ and add it to project as a resource.

Step 3: Core Data switcheroo

This code taken from Apple example project Core Data Books

Replace the method ‘persistentStoreCoordinator’ within AppDelegate with this one. Notice places in the code to fill in for your respective DB names.

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }
 
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"PREPOPULTED_NAME.sqlite"];
 
    /*
     Set up the store.
     For the sake of illustration, provide a pre-populated default store.
     */
    NSFileManager *fileManager = [NSFileManager defaultManager];
    // If the expected store doesn't exist, copy the default store.
    if (![fileManager fileExistsAtPath:[storeURL path]]) {

        // typically the main store name is 'appName.sqlite'
        NSURL *defaultStoreURL = [[NSBundle mainBundle] URLForResource:@"YOURAPPNAME" withExtension:@"sqlite"];
        if (defaultStoreURL) {
            [fileManager copyItemAtURL:defaultStoreURL toURL:storeURL error:NULL];
        }
    }
 
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
 
    NSError *error;
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
        /*
         Replace this implementation with code to handle the error appropriately.
         
         abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 
         
         Typical reasons for an error here include:
         * The persistent store is not accessible;
         * The schema for the persistent store is incompatible with current managed object model.
         Check the error message to determine what the actual problem was.
         
         
         If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
         
         If you encounter schema incompatibility errors during development, you can reduce their frequency by:
         * Simply deleting the existing store:
         [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
         
         * Performing automatic lightweight migration by passing the following dictionary as the options parameter:
         @{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}
         
         Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
         
         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
 
    return _persistentStoreCoordinator;
}

Step 4: Profit

That’s it! Hopefully this approch worked out and your app can now ship with pre-populated data.


Effective web API testing on iOS

» 09 Mar 2013 »

Permalink

Many iOS apps contain code which relies on a third-party web service for data or another type of behavior. Testing this interface is often ad-hoc at best. Any errors encountered are swallowed into the abyss of ‘that only happens with bad connectivity …’ Development which relies on the availability of an interface can also be problematic; such as requiring VPN access to hit a not-yet released server, or other factors. This article contains a sample walk-through of testing an imaginary web service with ILTesting, AFNetworking and Kiwi. It’s assumed the reader is familiar with the basics of Cocoa Touch, XCode and project creation.

If you’re a solid iOS dev but not familiar with Kiwi or AFnetworking, please check them out:

Kiwi

AFNetworking

It should be noted the approach shown using ILTesting will work with most any testing framework or networking library on iOS.

Example DogPark Application

The example application repo is located here on Github: DogPark

In our example application, we’ll be testing an implementation of an imaginary Dog Park API client. The client sends requests over HTTP and presently has two methods:

@interface HKZDogParkAPIClient : NSObject
- (void)fetchProfile:(int)profileId block:(void(^)(NSDictionary *dog, NSError* error))clientBlock;
- (void)signup:(void(^)(NSDictionary *dog, NSError* error))clientBlock;
@end

We’ll get to the fun of testing a Dog Park API soon, but first a little about the network testing library we’ll be using.

Intro To ILTesting (GitHub)

The magic behind ILTesting is simple: it ingeniously registers itself as an NSURLPrototcol handler thereby getting “first dibs” to handle HTTP requests from the application. (A more technically in-depth article on NSURLProtocol is available here : NSHipster .)

The registration of a pre-defined HTTP handler effectively keeps our app from hitting the network. (This speeds up our tests!) But the main point is predictively crafting an HTTP response for a given URL.

(If you’re wondering ‘why not just mock AFNetworking instead’, I shy away from mocking code that I did not write myself.)

Let’s take a look at a couple ILTesting class methods:

// beforeEach/afterEach are Kiwi constructs
 
beforeEach(^{
        //register as Protocol delegate, the magic begins
        [NSURLProtocol registerClass:[ILCannedURLProtocol class]];
       
        // Default HTTP status code
        [ILCannedURLProtocol setCannedStatusCode:200];
 
        // Configure ILtesting only for certain verbs if you like
        [ILCannedURLProtocol setSupportedMethods:[[NSArray alloc] initWithObjects:@"GET",@"POST", @"PUT", nil]];
        
        // Default headers. Ill be returning JSON here.
        NSMutableDictionary *headers = [[NSMutableDictionary alloc] init];
        [headers setObject:@"application/json; charset=utf-8" forKey:@"Content-Type"];
        [ILCannedURLProtocol setCannedHeaders: headers];
    });
    
    afterEach(^{
        // We must unregister the class after each test.
        [NSURLProtocol unregisterClass:[ILCannedURLProtocol class]];
    });

The ILTesting Delegate

ILTesting allows for delegation of client response data to a class which implements the protocol.

The protocol-delegate protocol looks like this and includes several optional methods:

@protocol ILCannedURLProtocolDelegate <NSObject>
- (NSData*)responseDataForClient:(id<NSURLProtocolClient>)client 
                         request:(NSURLRequest*)request;
@optional
- (BOOL)shouldInitWithRequest:(NSURLRequest*)request;
- (NSURL *)redirectForClient:(id<NSURLProtocolClient>)client 
                     request:(NSURLRequest *)request;
- (NSInteger)statusCodeForClient:(id<NSURLProtocolClient>)client
                         request:(NSURLRequest*)request;
- (NSDictionary*)headersForClient:(id<NSURLProtocolClient>)client 
                          request:(NSURLRequest*)request;
@end

Within the sample application, the delegate is implemented in HKZFakeWebsServer.

A tip for dealing with large APIs is to have several different classes each implement CannedURLProtocol and break up response testing between various functional areas. Ex - billing, user authentication, etc.

Enough stuffy code talk, let’s get going and have some fun!

Back to the DogPark

image

So anyway, we need to test our example APIClient methods. When beginning network testing, this process has proved successful:

  1. Capture the desired JSON response from the API and save it within a file. (In the sample application, these are kept within the DogParkTests/JSON folder.

  2. Create a trigger within the ILTesting delegate to serve the response.

  3. Write a test which utilizes the response.

Take a look at HKZFakeWebserver which implements ‘responseDataForClient’ which is self explanitory ;) - it loads various JSON files as response data when specific URLs are requested. The request is matched on HTTP verb and path.

@implementation HKZFakeWebserver
 // This function returns the correct JSON data, or whatever for the requested URL
// via the ILURLProtocol
 - (NSData *)responseDataForClient:(id<NSURLProtocolClient>)client 
                           request:(NSURLRequest*)request {
  NSData *responseData = nil;
    // Dog profile information
    if ([request.URL.absoluteString isEqual:@"http://dogpark.net/api/profile?id=33"] &&
            [request.HTTPMethod isEqualToString:@"GET"]) {
                NSBundle *bundle = [NSBundle bundleForClass:[self class]];
                NSString *resource = [bundle pathForResource:@"profile" ofType:@"json"];
                responseData = [[NSData alloc] initWithContentsOfFile:resource];
            }
    // Error handling for DogPark signup 
    else if ([request.URL.absoluteString isEqual:@"http://dogpark.net/api/new"] &&
         [request.HTTPMethod isEqualToString:@"POST"]) {
            // Post data, if we need it is here
            // NSString *postString = [[NSString alloc] initWithData:[request HTTPBody] 
                                                            encoding:NSUTF8StringEncoding];
            NSBundle *bundle = [NSBundle bundleForClass:[self class]];
            NSString *resource = [bundle pathForResource:@"signupError" ofType:@"json"];
            responseData = [[NSData alloc] initWithContentsOfFile:resource];
        }
	return responseData;
}
@end

Taking things further - Completing the implementation

Within the sample application, both tests presently fail. I leave the implementation of the client to the reader as an exercise in using ILTesting. The example code also serves as a reference to employ this testing method in your own project.

Happy testing!

References:

1) Original idea of injecting test data via NSURLProtocol goes to Claus Broch : www.infinite-loop.dk

2) Article ‘Using nsurlprotocol for injecting test data’


There are no couches in the woodshop

» 07 Mar 2013 »

Dont waste time ‘thinking’ at a computer. Step away, and draw out the idea on paper, a whiteboard, lipstick on a mirror - whatever. This media is static - which is important if any thinking is to occur. Only after there’s something to say should the computer be in use. Remember, a computer is a distraction machine. You can’t check twitter while using a belt sander or Miter Saw.

The woodshop is for DOING things. There is a reason people don’t hang out in the woodshop on a couch chatting or browsing catalogs. Treat your time at the computer as you would sitting at a workbench. You are paid for your thoughts. It just so happens you also know howto express your thoughts as executable code to a machine.


Checkout All Posts