iPhone – Core Data and UITableView. A drill-down application

UPDATED 2010-04-03

iPhone OS 3.0 is out this week and while the new iPhone 3GS was pretty disappointing hardware wise, the new iPhone OS is really packed with new cool frameworks and APIs. I’m working on a lot of projects back and fourth depending on what I feel like working with at the moment. After we released the first app, Hitta Hem, I wrote a bunch of articles based on that work and got kind of tired of iPhone development. I’ve started working again on some new things around iPhone OS 3.0 features and I thought I’d share some Core Data stuff.

What is Core Data?

Core Data is new in iPhone OS 3.0, but has been around for OS X application developers for quite some time. Since I’m NOT an OS X application developer this framework was totally new to me and it’s a great addition to the iPhone SDK. So what exactly is the Core Data framework? If you are familiar with the entity framework in .NET, it’s something similar. Core Data is an abstraction layer on top of a sqlite database which enables developers to more easily implement data-centric applications. Personally I never got around to do much with sqlite but I touched it enough to appreciate what Core Data brings to the table. My main issues with sqlite are poor tooling support and a limited range of documentation and samples. I’m used to working with Microsoft products and even though I’m aware that MS has the best tooling support in the world, the tooling provided for sqlite sucks!

What the Core Data framework provide is an easy way to model your data storage around entities (classes) with relationships between them. If you’re not familiar with relational databases such as SQL, Core Data is for you! Core Data is a model first framework that generate the database and tables automatically from your model, it can also generate classes for your model entities. However, if you have worked with the entity framework in .NET, you’ll get a bit disappointed over the limited functionality. But we have to remember, the iPhone is just a phone.

The sample application

This application is simple and not for much use as it is, but it’s a useful example for a real application.

Level1Level2Level3

I’ve placed the source data in an XML file which is in the application bundle, the XML is parsed and stored in the object model. The data is made up of Swedish counties, provinces and cities where each county has multiple provinces and the provinces has multiple cities. In SQL-terms they have a one-to-many relationship. You can see the model on the picture below.

Core data modelIf you read up on Apples’ documentation they encourage everyone to add inverse relationships for all relationships in order to keep the data consistent. In my model I’ve added a one-to-many relationship from County to Province (named CountyToProvince) with action set to cascade. That means if a County is removed so are all provinces related to it. The inverse relationship is called “ProvinceToCounty” on the Province object. The inverse relationship enables you to access the County object of a Province object.
The province entity also has a one-to-many relationship but with City, called “ProvinceToCcounty”. City also has an inverse relationship to Province called “CityToProvince”.

The reason for cascading events is that we can easily clean up our entire model by just removing the top level (County).

sqlite databaseIf you’re interested in how things actually work (like me) you can find and peek into the sqlite database created from your model. The iPhone simulator stores application files in /Users/<your user name>/Library/Application Support/iPhone Simulator/User/Applications/<random guid>/ and you will find a file named Location.sqlite inside the “Documents”-folder.
To the left is an image of what the database actually looks like inside (using SQLite Manager, the Firefox plug in(!?)). As you can see the Core Data framework created two additional tables other than what we actually modeled in the designer and for some reason the Apple engineers love the letter Z. With SQLite Manager you can also take a look inside the tables to see the values. The relationships work like you would expect them to, the tables reference each other by the primary key which is just an integer that gets incremented.

Feeding the application

I decided to feed the application using XML because it’s both simple and makes sense for a real world application. You could easily request the XML from a web service and store it in your app and not have to read live from the web all the time. Of course, if your data is massive, this could be a problem but you could also split the data requests into smaller chunks and just store what your fetch for re-use and perhaps an offline mode.

Here’s what my XML structure looks like

<loc>
 <county name="Norrbotten">
 <province name="Arjeplog">
 <city name="Arjeplog"/>
 </province>
 </county>
</loc>

I’ve created a parser for my XML. When it reads a county-element it asks the object model context for a new County object. If there are province-elements they are created and added to the County objects’ CountyToProvince relation collection. If there are city-elements they are created and added to the County object. When there are no more cities and provinces the end-element event fires for the county-element and that’s when I store all changes which means they are persisted down into the sqlite database by Core Data.

Creating a object from the Core Data context is easy as pie:

currentCounty = (County *)[NSEntityDescription insertNewObjectForEntityForName:@"County" inManagedObjectContext:managedObjectContext];

Adding a Province child object to a County is done using the method addCountyToProvinceObject which has been generated for us automatically:

[currentCounty addCountyToProvinceObject:currentProvince];

Fetching objects from the object model

Fetching data from the object model is very easy, no SQL skills required, almost. To simplify fetching data and reducing the amount of code I created a simple helper class with static methods that do most of the work.

+(NSMutableArray *) searchObjectsInContext: (NSString*) entityName : (NSPredicate *) predicate : (NSString*) sortKey : (BOOL) sortAscending : (NSManagedObjectContext *) managedObjectContext
{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
 
// If a predicate was passed, pass it to the query
if(predicate != nil)
{
[request setPredicate:predicate];
}
 
// If a sort key was passed, use it for sorting.
if(sortKey != nil)
{
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortKey ascending:sortAscending];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
}
 
NSError *error;
 
NSMutableArray *mutableFetchResults = [[[managedObjectContext executeFetchRequest:request error:&amp;error] mutableCopy] autorelease];
 
[request release];
 
return mutableFetchResults;
}
 
+(NSMutableArray *) getObjectsFromContext: (NSString*) entityName : (NSString*) sortKey : (BOOL) sortAscending : (NSManagedObjectContext *) managedObjectContext
{
return [self searchObjectsInContext:entityName :nil :sortKey :sortAscending :managedObjectContext];
}

You can either retrieve all objects of a given type buy calling getObjectsFromContext. All you have to do is pass the entity name of your object and the rest is optional except the managedObjectContext. It’s very useful to let Core Data sort your data by passing the sortKey, which is the name of the column, or property if you like, to sort by. Sorting can be done ascending or descending to you liking.
If you only want to retrieve specific parts of you data you must use the NSPredicate object to define which object you want. A predicate for getting all child objects of a given parent object can be done like so:

predicate = [NSPredicate predicateWithFormat:@"(ProvinceToCounty == %@)", selectedObject];

where selectedObject is the parent object of the children you want to fetch. Just pass the predicate and the entity name of the objects you want to searchObjectsInContext and you get them back as a mutable array. The helper class is just to reduce the amount of duplicated code needed to fetch objects.

In my application the UITableView will load either all entities of a given entity name or filter them down by a predicate. This way the same UITableView class is used for all views in the application. When a row is selected a predicate is created to get the child objects of the selected entity and then passed to a new instance of RootViewController:

// Override to support row selection in the table view.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
if(self.entityName == @"County" || self.entityName == @"Province")
{
// Create a new table view of this very same class.
RootViewController *rootViewController = [[RootViewController alloc]
initWithStyle:UITableViewStylePlain];
 
// Pass the managed object context
rootViewController.managedObjectContext = self.managedObjectContext;
NSPredicate *predicate = nil;
NSManagedObject *selectedObject = [entityArray objectAtIndex:indexPath.row];
 
if(self.entityName == @"County")
{
rootViewController.entityName = @"Province";
 
// Create a query predicate to find all child objects of the selected object.
predicate = [NSPredicate predicateWithFormat:@"(ProvinceToCounty == %@)", selectedObject];
 
}
else if(self.entityName == @"Province")
{
rootViewController.entityName = @"City";
 
// Create a query predicate to find all child objects of the selected object.
predicate = [NSPredicate predicateWithFormat:@"(CityToProvince == %@)", selectedObject];
}
 
[rootViewController setEntitySearchPredicate:predicate];
 
//Push the new table view on the stack
[self.navigationController pushViewController:rootViewController animated:YES];
[rootViewController release];
}
}

Final words

There you have it. Core Data is an awesome addition to the iPhone SDK and if you use it wisely you can improve your application by saving lots of expensive server round-trips to fetch data and perhaps also store data for offline usage.

Remember, if you make changes to an existing data model your app will crash with an exception: NSInternalInconsistencyException. This is because the database which was previously created doesn’t match your new model. You can code your app to remove the database file and then create a new. My updated example contains the code.

* UPDATED 2010-04-03: Fixed warnings reported by XCode analyzer. Added a call button and an in-app-email button to the details page.

Download the sample project!

161 thoughts on “iPhone – Core Data and UITableView. A drill-down application

  1. excellent post, but I have a question how I can do to get a hearing before a button that takes me to TableView I hope you can help me

  2. Good afternoon ,

    i have a slight problem with the retrieval of the xml file , im downloading the file at runtime and saving it in the documents directory , but i have no idea how to apply your code to facilitate for the retreval of this file … thanks for all the help in advance

    NSURL *xmlURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@”data” ofType:@”xml”]];

    -Chris

  3. I’m trying to implement this sample code in a tabBar Application but none of my attempts are successful. Can this be implemented in a tabBar App? Delegation is my concern as I know the architecture of tabBarController differs from the NavigationController.

    Any kind guidance from you sir would be hugely appreciated!

  4. thanks for the post it really good.but i want to implement in the xcode 4.2…could u help me how to do in that

  5. Hi Bjorn,

    I see that there has been a lot of feedback on this cool tutorial/example. Nice work! I was wondering why the email button wouldn’t have illustrated sending the data from the record/object in context? Seems like it would have been a cool thing. Surprised no-one else asked about it?…

    Thanks,
    Adam

  6. If I wanted to sort the list on a column other than ‘Name’ (email, for example), how would I do this?

  7. I am looking through your code and It’s very cool. However, I would like to go directly from the Province view to a detailed view of the Province. Eliminating the City view. Could you please explain in general what needs to be done?

    Thanks
    Keven

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>