Zend certified PHP/Magento developer

BuildMobile: Managing Information with CoreData

In this series, we’ve been creating an application called Orny. The application will (hypothetically) be used by Ornithologists to record sightings of birds, where those sightings occurred, and will possibly even store photographs taken by the user.

At the moment, we’re listing some species of birds in an array. In the near future, we’re going to want to store data, so we’re going to make our app use CoreData. In this tutorial we’re going to cover the basics of CoreData, how to create a default database, and how to retrieve information from it. In the next article we’ll discuss how to store sighting data in the database – which will bring us close to having a useful application.

Creating the Schema

The first thing we’re going to do is create a schema for our application. If you’ve worked with databases before, this should be reasonably familiar.

A schema is a collection of “entities” (often referred to as “tables” in the web development world). Each entity has attributes that describe it, and can have relationships with other entities. A “chair” entity, for example, might have four related entities called “legs”. You can access the specific legs of a chair entity using an accessor method like so: chair.legs (which would presumably return an array).

We’re going to avoid getting into the nitty-gritty of entity relationships for now, but they’re not terribly complex.

When we first created our project, we told Xcode that we wanted to use managed CoreData entities, so we already have a data model in place.

Orny Core Data Figure 1

Figure 1

Click on Orny.xcdatamodeld (this is the data model definition file).

You should see one of the following two screens:

Orny Core Data Figure 1

Figure 1

Orny Core Data Figure 3

Figure 3

You can switch between them using ‘Editor Style’ in the bottom right.

To add an entity, click the ‘Add Entity’ button.

Orny Core Data Figure 4

Figure 4

This button can sometimes be labelled “Add Fetch Request” or “Add Configuration” depending on what you’ve added previously – in which case, do a long-click on the button, and you’ll see the option to “Add Entity”

Create an entity called Species.

Orny Core Data Figure 5

Figure 5

Click the ‘+’ button under Attributes to add some attributes – name, filename, and text_description.

Orny Core Data Figure 6

Figure 6

We need to specify the types of these attributes – they should all be strings. Next to each attribute, select the drop-down for type, and change them accordingly.

Orny Core Data Figure 7

Figure 7

Adding Managed Model Classes

We’ve now created our schema, but we need to create in-code representations of the entities we’re managing. iOS calls these “Models” (as in “Model, View, Controller”). We request sub-sets of the data in the database using fetch requests, which will return arrays of models. Each instance of a model class represents a row in the database (or at least, does so once we’ve saved it to the database.)

The models that CoreData gives us are typically ‘managed’ – if we make changes to them, and then tell the relevant persistent storage controller to do a ‘save’, it will save any changes that we have made to any of our model instances. (It’s essentially the Active Record pattern, which has its strengths and weaknesses but is fairly well understood.)

If you don’t have it already, create a “Models” folder in your project (Right-click ‘Orny’ in the file browser, and hit ‘New Group’.)

Right-click your new Models group, and click ‘New File’.

Select ‘Core Data’ and ‘NSManagedObject subclass’.

Orny Core Data Figure 8

Figure 8

Hit ‘Next’, and select the ‘Orny’ data model.

Orny Core Data Figure 9

Figure 9

Hit ‘Next’, select ‘Species’

Orny Core Data Figure 10

Figure 10

Name the file Species, and you’re done. This creates a model class – Species.m, the header of which looks like this:

@interface Species : NSManagedObject {
    @private
}
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSString * text_description;
@property (nonatomic, retain) NSString * filename;
@end

You can provide your own getters and setters if you wish, to enforce business logic on your model. It’s typically best to put a lot of your logic into the models, and leave the controllers as “thin” as possible – but this is a very simple class that we’re not going to modify yet.

Querying and Fetching Results

Our appDelegate already has most of the methods we need to access our database. Have a look at OrnyAppDelegate.m, and specifically the methods managedObjectContext, managedObjectModel, persistentStoreCoordinator and saveContext.

persistentStoreCoordinator instantiates our database from a .sqlite file, and correlates the contents of that file with our managed object model via managedObjectModel. We’ll modify this later to copy in our database if none exists at launch (see below).

managedObjectContext gets the managed object context (well surprise surprise!), which is the correlation of the database file, the managed object model, and a controller that is used to fetch results and save data. NSManagedObjectContext is the class we’ll be interacting with most.

Our purposes here are to store data about bird species in the database, and display this data to the user. Displaying the data happens in our BirdListViewController, so let’s change the loadData method there as follows:

-(void)loadBirdData {
    // The context is, roughly, the "database schema"
    NSManagedObjectContext *context = [(OrnyAppDelegate*)[[UIApplication sharedApplication] delegate] managedObjectContext];
    // A request is like an SQL select statement; we're retrieving some set of objects
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    // An entity description is used to specify which entit(y|ies) we want to pull from the context
    NSEntityDescription *description = [NSEntityDescription entityForName:@"Species" inManagedObjectContext:context];
    [request setEntity:description];
    // A sort descriptor lets us order the results
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO];
    [request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
    // A fetchedResultsController handles the fetching of our data
    NSFetchedResultsController *fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
    fetchController.delegate = self;
    NSError *error = nil;
    if(![fetchController performFetch:error]) {
        // TODO: Handle error
        abort();
    }
    birds = fetchController.fetchedObjects;
}

There’s a fair bit going on here, but the comments should explain it a bit. In essence, we’re requesting a bunch of objects from the database where previously we were creating an ‘NSMutableArray’ to store that information.

Ooh. We’ve changed the format of our data structures. Previously we’d call [birds objectAtIndex:someIndex] to get a particular “row” in our data set. We’d then call [species objectForKey:@"name"] or the like to get an attribute of that species.

We need to rewrite some more of our BirdListViewController, specifically tableView:tableView cellForRowAtIndexPath:indexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *newCell;
    if((newCell = [tableView dequeueReusableCellWithIdentifier:@"birdList"]) == nil) {
        newCell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"birdList"] autorelease];
    }
    //NSDictionary *thisBird = [birds objectAtIndex:[indexPath row]];
    Species *thisBird = [birds objectAtIndex:[indexPath row]];
    UILabel *newCellLabel = [newCell textLabel];
    //[newCellLabel setText:[thisBird objectForKey:@"name"]];
    [newCellLabel setText:thisBird.name];
    return newCell;
}

We’ll also need to rewrite tableView:tableView didSelectRowAtIndexPath:indexPath:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:NO];
    //NSDictionary *thisBird = [birds objectAtIndex:[indexPath row]];
    Species *bird = [birds objectAtIndex:[indexPath row]];
    BirdListDetailViewController *detail = [[BirdListDetailViewController alloc] initWithNibName:@"BirdListDetailViewController" bundle:[NSBundle mainBundle]];
    //detail.filename = [thisBird objectForKey:@"image"];
    detail.filename = bird.filename;
    [[self navigationController] pushViewController:detail animated:YES];
    [detail release];
}

I’ve left our previous code mostly in place, but commented out, so you can see the difference (but you can see much more if you have a look through BuildMobile’s GitHub repository for Orny. Reading the source and commits is a great way to learn!)

If you run the application now, it should work – but you won’t see any species listed. That’s because our database starts out empty, and we need to pre-populate it…

Pre-populating Data

One of the places where CoreData and Xcode in general does not shine is the process of creating an initial database. While you’ve created the schema and a model, your choices for actually creating the database file are:

  1. Manually creating the .sqlite file
  2. Making your app save a copy of the empty database, then modifying that to become your default database
  3. Populating your app’s database at first launch (which can be problematic for users who might not be connected to the Internet)

A dismal state of affairs, but there is a solution. We’ll make our app save a copy of our empty database, modify it to contain some data, and save our default database to our ‘Supporting Files’ group. Then, when our app runs, we’ll make it check to see if a database exists – and if not, we’ll copy in our default database.

This approach works fine for a simple app, but be aware that if you subsequently want to add data to the database when you release a new version of your app, you’re going to have to do something special to merge your new data with the user’s existing database. One solution is to use two databases, one to store your app’s data and one to store your user’s. Further discussion goes a bit beyond the scope of this tutorial, however – back to the point!

Let’s force the app to save us an empty database. We’ll modify BirdListViewController’s loadBirdData method to do this – add the following to the end of that function:

// Adding a saveContext call, to generate an empty sqlite db
[(OrnyAppDelegate*)[[UIApplication sharedApplication] delegate] saveContext];

We’re telling the AppDelegate to save our managedObjectContext. Neato. Run the application – you won’t see much happen, but we should now be able to find the .sqlite database in…

/User/_username/Library/Application Support/iPhone Simulator/_sdk version/Applications/_some magic string_/Documents/Orny.sqlite

That magic string is automatically generated by the Simulator, so you might need to look at the last modified timestamps of the folders to work out which one is your application. Such a pain.

Once you have that .sqlite file, drag-and-drop it to your application’s Supporting Files group, and rename it Orny_default.sqlite.

Orny Core Data Figure 11

Figure 11

Now we want to modify the database file. I’m currently using SQLite Database Browser, but any tool that can read and modify .sqlite database files should be fine.

Once you have that app installed, you should be able to modify the file by right-clicking and selecting ‘Open with External Editor’.

Orny Core Data Figure 12

Figure 12

In SQLite Browser, hit the ‘Browse Data’ tab, and select ‘ZSPECIES’ from the table drop-down.

Orny Core Data Figure 13

Figure 13

Insert a row, as per Figure 13.

With that done, we also need to modify the PRIMARYKEY table. This is where CoreData keeps a record of the Entities it’s managing for this database, the name of the Entity, and the number of entries in the database. Modify the Z_MAX column as per Figure 14.

Orny Core Data Figure 14

Figure 14

Hit the save icon, and we’re done modifying the database.

You could also use a script to create your default database – see Ray Wenderlich’s article on how to do this with Python

Copying in our Default Database

We’re not quite done yet. We need to modify our persistentStoreController method on our AppDelegate to copy in our default database if none exists yet.

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
    //[[self applicationDocumentsDirectory] stringByAppendingPathComponent:];
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Orny.sqlite"];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if(![fileManager fileExistsAtPath:[storeURL path]]) {
        NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"Orny_default" ofType:@"sqlite"];
        if (defaultStorePath) {
            NSLog(@"COPYING");
            NSLog(@"%@", [storeURL relativeString]);
            NSLog(@"%@", defaultStorePath);
            NSError *error;
            if(![fileManager copyItemAtPath:defaultStorePath toPath:[storeURL path] error:error]) {
                NSLog(@"FILE COPY ERROR: %@", [error localizedDescription]);
            }
        }
    }
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    ...

I’ve truncated the listing above for brevity, but you can see the lines we’re copying in.

I’ve also left some debugging method calls in place. I was having trouble copying the file in, and needed to see the relevant file paths that were being used. This is a quick and dirty way to get some debugging information, but it’s much more efficient to set breakpoints and use the debugger to explore these sorts of issues (we’ll cover that in another tutorial).

Okay, run the application and you should be roughly back where we started in terms of functionality – but now we’re reading from a database. Go team!

Error Handling

The default CoreData methods on our OrnyAppDelegate are heavily commented by Apple, and implement some very limited error handling for us. If you ever want to publish an app to the app store, you must read these comments, and handle errors elegantly. How you handle an error is up to you – you might re-create the database from scratch, or just show the user a message asking them to re-start the app, but you should definitely do something.

I’ll leave that adventure for you!

Conclusion

We’ve now learnt how to use CoreData to retrieve information from a database. We haven’t looked at how to modify data yet – we’ll save that for a later tutorial – but we’ve taken a big step to making our application more interactive in the future.

The “Orny” Series

Andy White is providing an intense meditation of developing apps on the iOS platform at BuildMobile. With the prerequisite of having a tasty refreshing beverage in hand, use the tag to all Orny articles, or jump straight into an article specifically from this index.