Of all of the ways to persist data on the iPhone, Core Data is the
best one to use for non-trivial data storage. It can reduce the memory
overhead of your app, increase responsiveness, and save you from writing
a lot of boilerplate code.
However, the learning curve for Core Data can be quite large. That’s
where this tutorial series comes in – the goal is to get you up to
speed with the basics of Core Data quickly.
In this part of the series, we’re going to create a visual data model
for our objects, run a quick and dirty test to make sure it works, and
then hook it up to a table view so we can see a list of our objects.
In the second part of the series, we’re going to discuss how to
import or preload existing data into Core Data so that we have some good
default data when our app starts up.
In the final part of the series, we’re going to discuss how we can
optimize our app by using NSFetchedResultsController, to reduce memory
overhead and improve response time.
Before proceeding with this tutorial, I recommend checking out my tutorial series on SQLite for iPhone Developers
first. On the iPhone, the backing store for Core Data is SQLite, so it
helps to have an understanding of how that works first. Plus, the app
we’re making is the same app we made in that tutorial – just with Core
Data this time!
Creating a Core Data Project
So let’s get started! Create a new Window-based Application, and
choose “Use Core Data for storage”, and name the project
“FailedBanksCD.”
Before we begin, let’s take a quick look at the project. First
expand Resources and double click on FailedBanksCD.xcdatamodel. You’ll
see a visual editor will pop up – this is what we’ll be using in a
minute to diagram our model objects. Go ahead and close it for now.
Then take a look at FailedBanksCDAppDelegate.m. You’ll see that
there are several new functions in here that are implemented for us, to
set up the Core Data “stack”. One creates a managed object context, one creates a managed object model, and one creates a persistent store coordinator. Huh??
Don’t worry. The names sound confusing at first, but once you get a
“mental shortcut” for what they’re all about they are easy to
understand.
- Managed Object Model: You can think of this as the database
schema. It is a class that contains definitions for each of the
objects (also called “Entities”) that you are storing in the database.
Usually, you will use the visual editor you just peeked at to set up
what objects are in the database, what their attributes, and how they
relate to each other. However, you can do this with code too!
- Persistent Store Coordinator: You can think of this as the
database connection. Here’s where you set up the actual names and
locations of what databases will be used to store the objects, and any
time a managed object context needs to save something it goes through
this single coordinator.
- Managed Object Context: You can think of this as a “scratch
pad” for objects that come from the database. It’s also the most
important of the three for us, because we’ll be working with this the
most. Basically, whenever you need to get objects, insert objects, or
delete objects, you call methods on the managed object context (or at
least most of the time!)
Don’t worry too much about these methods – you won’t have to mess
with them much. However, it’s good to know that they are there and what
they represent.
Defining Our Model
When we created our database tables in the SQLite tutorial, we had a
single table containing all of the data for a failed bank. To reduce
the amount of data in memory at once (for learning purposes), we just
pulled out the subset of the fields we needed for display in our first
table view.
So we might be tempted to set up our model the same way with Core
Data. However, with Core data you cannot retrieve only certain
attributes of an object – you have to retrieve the entire object.
However, if we factor the objects into two pieces – the FailedBankInfo
piece and the FailedBankDetails piece – we can accomplish the exact same
thing.
So let’s see how this will work. Open up the visual model editor
(expand Resources and double click FailedBanksCD.xcodedatamodel).
Let’s start by creating an object in our model – referred to as
“Entity” in Core Data speak. In the top left pane, click the plus
button to add a new Entity like the following:
Upon clicking the plus, it will create a new Entity, and show the
properties for the Entity in the right panel like the following:
Name the Entity FailedBankInfo. Note that it currently lists the
class as a subclass of NSManagedObject. This is the default class for
Entities, which we’ll use for now – later we’ll come back and set up
custom objects.
So let’s add some attributes. First, make sure that your Entity is
selected by either clicking on the Entity name in the left panel, or the
diagram for the entity in the diagram view. In the middle panel, click
the plus button and then click “Add Attribute” like the following:
In the property pane on the right, name the attribute “name” and set the Type to “String” like the following:
Now, repeat this to add two more attributes, “city” and “state”, also both strings.
Next, we need to create an entity for FailedBankDetails. Create an
Entity the same way you did before, and add the following attributes to
it: zip of type Int 32, closeDate of type Date, and updatedDate of type
Date.
Finally, we need to associate these two types. Select
FailedBankInfo, and click the plus button in the middle pane, but this
time select “Add relationship”:
Name the relationship “details”, and set the destination as “FailedBankDetails.”
Ok, so what did we just do here? We just set up a relationship in
Core Data, which links up one entity to another entity. In this case,
we are setting up a one-to-one relationship – every FailedBankInfo will
have exactly 1 FailedBankDetails. Behind the scenes, Core Data will set
up our database so that our FailedBankInfo table has a field for the ID
of the corresponding FailedBankDetails object.
Apple recommends that whenever you create a link from one object to
another, you create a link from the other object going back as well. So
let’s do this.
Now add a relationship to “FailedBankDetails” named “info”, set the
destination to “FailedBankInfo”, and set the inverse to “details”.
Also, set the delete rule for both relationships to “cascade”. This
means that if you delete one object with Core Data, Core Data will
delete the associated object as well. This makes sense in this case
because a FailedBankDetails wouldn’t mean anything without a
corresponding FailedBankInfo.
Testing our Model
Believe it or not, that was probably the most important thing we
needed to do. Now it’s just a matter of testing out using Core Data and
making sure it works!
First, let’s test out adding a test object to our database. Open
FailedBanksCDAppDelegate.m and add the following to the top of
applicationDidFinishLaunching:
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObject *failedBankInfo = [NSEntityDescription
insertNewObjectForEntityForName:@"FailedBankInfo"
inManagedObjectContext:context];
[failedBankInfo setValue:@"Test Bank" forKey:@"name"];
[failedBankInfo setValue:@"Testville" forKey:@"city"];
[failedBankInfo setValue:@"Testland" forKey:@"state"];
NSManagedObject *failedBankDetails = [NSEntityDescription
insertNewObjectForEntityForName:@"FailedBankDetails"
inManagedObjectContext:context];
[failedBankDetails setValue:[NSDate date] forKey:@"closeDate"];
[failedBankDetails setValue:[NSDate date] forKey:@"updatedDate"];
[failedBankDetails setValue:[NSNumber numberWithInt:12345] forKey:@"zip"];
[failedBankDetails setValue:failedBankInfo forKey:@"info"];
[failedBankInfo setValue:failedBankDetails forKey:@"details"];
NSError *error;
if (![context save:&error]) {
NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
}
|
In the first line, we grab a pointer to our managed object context
using the helper function that comes included with the template.
Then we create a new instance of an NSManagedObject for our
FailedBankInfo entity, by calling insertNewObjectForEntityForName.
Every object that Core Data stores derives from NSManagedObject. Once
you have an instance of the object, you can call setValue for any
attribute that you defined in the visual editor to set up the object.
So we go ahead adn set up a test bank, for both FailedBankInfo and
FailedBankDetails. At this point the objects are just modified in
meomry – to store them back to the database we need to call save on the
managedObjectContext.
That’s all there is to it to insert objects – no SQL code necessary!
Before we try this out, let’s add some more code in there to list out all the objects currently in the database:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:@"FailedBankInfo" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
for (NSManagedObject *info in fetchedObjects) {
NSLog(@"Name: %@", [info valueForKey:@"name"]);
NSManagedObject *details = [info valueForKey:@"details"];
NSLog(@"Zip: %@", [details valueForKey:@"zip"]);
}
[fetchRequest release];
|
Here we create a new object called a fetch request. You can
think of a fetch request as a SELECT clause. We call entityForName to
get a pointer to the FailedBankInfo entity we want to retrieve, and then
use setEntity to tell our fetch request that’s the kind of Entity we
want.
We then call executeFetchRequest on the managed object context to
pull all of the objects in the FailedBankInfo table into our “scratch
pad”. We then iterate through each NSManagedObject, and use valueForKey
to pull out various pieces.
Note that even though we pulled out just the objects from the
FailedBankInfo table, we can still access the associated
FailedBankDetails object by acessing the details property on the
FAiledBankInfo entity.
How does this work? Behind the scenes, when you access that property
Core Data notices that it doesn’t have the data in the context, and
“faults”, which basically means it runs over to the database and pulls
in that data for you right as you need it. Pretty convenient!
Give this code a run and take a look in your output window, and you
should see a test bank in your database for every time you run the
program.
Seeing the Raw SQL Statements
I don’t know about you, but when I’m working on this kind of stuff I
really like to see the actual SQL statements going on to understand how
things are working (and make sure it’s doing what I expect!)
Once again Apple has provided an easy solution to this. Open the
Executables drop-down in XCode and find your FailedBanksCD executable.
Right click on that and click “Get Info.” Navigate to the Arguments tab
and add the following argument: “-com.apple.CoreData.SQLDebug 1″. When
you’re done it should look like the following:
Now when you run your code, in the debug output you should see useful trace statements like this showing you what’s going on:
SELECT Z_VERSION, Z_UUID, Z_PLIST FROM Z_METADATA
SELECT Z_MAX FROM Z_PRIMARYKEY WHERE Z_ENT = ?
UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_ENT = ? AND Z_MAX = ?
INSERT INTO ZFAILEDBANKDETAILS(Z_PK, Z_ENT, Z_OPT, ZINFO,
ZUPDATEDDATE, ZZIP, ZCLOSEDATE) VALUES(?, ?, ?, ?, ?, ?, ?)
INSERT INTO ZFAILEDBANKINFO(Z_PK, Z_ENT, Z_OPT, ZDETAILS, ZNAME,
ZSTATE, ZCITY) VALUES(?, ?, ?, ?, ?, ?, ?)
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY, t0.ZDETAILS
FROM ZFAILEDBANKINFO t0
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZUPDATEDDATE, t0.ZZIP, t0.ZCLOSEDATE,
t0.ZINFO FROM ZFAILEDBANKDETAILS t0 WHERE t0.Z_PK = ?
So here we see things are working as we expect. The first two
selects and update are Core Data doing some bookkeeping work keeping
track of what the next ID for the entity should be.
Then we have our inserts into the details and info tables. After
that, we select the entire bank info table in our query. Then as we
iterate through the results, each time we access the details variable,
behind the scenes Core Data faults and issues another select statement
to get the data from the ZFAILEDBANKDETAILS table.
Auto Generating Model Files
So far, we’ve been using NSManagedObject to work with our Entities.
This isn’t the best way to do things, because it’s quite easy to make a
mistake and type an attribute name incorrectly, or set data using the
wrong type, etc.
The better way to do things is to create a Model file for each
entity. You can do this by hand, but XCode makes this quite easy with a
class generator.
Let’s try it out. Open up FailedBanksCD.xcdatamodel, click on the
FailedBankInfo entity, and go to File\New File. Select “Cocoa Touch
Class”, and you should see a new template for “Managed Object Class.”
Select this and click Next, and then click Next again on the following
view.
In the third view, XCode will allow you to select the Entities to
generate classes for. To save time, make sure BOTH FailedBankInfo and
FailedBankDetails are checked, and click Finish.
You should see some new files added to your project:
FailedBankInfo.h/m and FailedBankDetails.h/m. These classes are very
simple, and just declare properties based on the attributes you added to
the entities. You will notice that the properties are declared as
dynamic inside the .m files – this is because Core Data handles wiring
the properties up automatically.
I did notice one problem with the autogenerated classes that I had to
fix. If you look at FailedBankDetails.h, you’ll notice that the info
variable is correctly declared as a FailedBankInfo class, but in
FailedBankInfo.h the details variable is defined as an NSManagedObject
(but it should be a FailedBankDetails object). You can fix this by
adding a predeclaration of FailedBankDetails to the top of the file:
@class FailedBankDetails;
|
And then changing the details property declaration to the following:
@property (nonatomic, retain) FailedBankDetails * details;
|
Also, take a peek back in FailedBanksCD.xcdatamodel. When you look
at the properties for the entities, you’ll notice that the classes have
now been set automatically to the names of the autogenerated classes:
Now, let’s tweak our test code in the app delegate to use our new
subclasses of NSManagedObject. First add the headers we’ll need up top:
#import "FailedBankInfo.h"
#import "FailedBankDetails.h"
|
Then change the code as follows:
NSManagedObjectContext *context = [self managedObjectContext];
FailedBankInfo *failedBankInfo = [NSEntityDescription
insertNewObjectForEntityForName:@"FailedBankInfo"
inManagedObjectContext:context];
failedBankInfo.name = @"Test Bank";
failedBankInfo.city = @"Testville";
failedBankInfo.state = @"Testland";
FailedBankDetails *failedBankDetails = [NSEntityDescription
insertNewObjectForEntityForName:@"FailedBankDetails"
inManagedObjectContext:context];
failedBankDetails.closeDate = [NSDate date];
failedBankDetails.updatedDate = [NSDate date];
failedBankDetails.zip = [NSNumber numberWithInt:12345];
failedBankDetails.info = failedBankInfo;
failedBankInfo.details = failedBankDetails;
NSError *error;
if (![context save:&error]) {
NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
}
// Test listing all FailedBankInfos from the store
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
for (FailedBankInfo *info in fetchedObjects) {
NSLog(@"Name: %@", info.name);
FailedBankDetails *details = info.details;
NSLog(@"Zip: %@", details.zip);
}
[fetchRequest release];
|
This is pretty much the same code we had before, except instead of
using NSManagedObject directly, we use our new subclasses. Now we have
type safety and cleaner code!
Creating a Table View
Right click on Classes and click “Add\New File…” and pick
“UIViewController subclass”, making sure “UITableVIewController
subclass” is checked and “With XIB for user interface” is NOT checked.
Name the class FailedBanksListViewController.
Open up FailedBanksListViewController.h and add a two member variables:
- A member variable/property for the failedBankInfos which we’ll retrieve from the database, just like we used last time.
- A member variable for the managed object context to use. Note we
could retrieve this from the application delegate, but it’s better
practice to have it passed in as a member variable to avoid
interdependence.
So when you’re done it should look like the following:
Switch over to FailedBanksListViewController.m and add some imports, your synthesize statement, and your cleanup code:
// At very top, in import section
#import "FailedBankInfo.h"
// At top, under @implementation
@synthesize failedBankInfos = _failedBankInfos;
@synthesize context = _context;
// In dealloc
self.failedBankInfos = nil;
self.context = nil;
|
Then uncomment viewDidLoad and modify it to look like the following:
- (void)viewDidLoad {
[super viewDidLoad];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:@"FailedBankInfo" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSError *error;
self.failedBankInfos = [_context executeFetchRequest:fetchRequest error:&error];
self.title = @"Failed Banks";
[fetchRequest release];
}
|
This code should look pretty familiar to our test code from earlier.
We simply create a fetch request to get all of the FailedBankInfos in
the database, and store it in our member variable.
The rest of the mods are exactly like we did in the SQLite tutorial.
For quick reference, I’ll list the remaining steps here again:
Return 1 for numberOfSectionsInTableView:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
|
Replace numberOfRowsInSection with the following:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return [_failedBankInfos count];
}
|
Modify cellForRowAtIndexPath to look like the following:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:CellIdentifier] autorelease];
}
// Set up the cell...
FailedBankInfo *info = [_failedBankInfos objectAtIndex:indexPath.row];
cell.textLabel.text = info.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@, %@",
info.city, info.state];
return cell;
}
|
Add an outlet into FailedBanksCDAppDelegate.h for the UINavigationController we’re about to add:
Open up Resources and double click on MainWindow.xib. Drag a
Navigation Controller from the library into the MainWindow.xib. Click on
the down arrow on the Navigation Controller that you just added, click
on the View Controller, over in the attribute panel switch to the fourth
tab, and switch the “Class” to “FailedBanksListViewController.”
Finally, control-drag from “FailedBanksCD App Delegate” in
MainWindow.xib to “Navigation Controller”, and connect it to the
“navController” outlet. Save the xib and close.
Now all we need to do is add a few lines to our FailedBanksCDAppDelegate.m:
// At top
#import "FailedBanksListViewController.h"
// Under @implementation
@synthesize navController = _navController;
// In applicationDisFinishLaunching, before makeKeyAndVisible:
FailedBanksListViewController *root = (FailedBanksListViewController *) [_navController topViewController];
root.context = [self managedObjectContext];
[window addSubview:_navController.view];
// In dealloc
self.navController = nil;
|
Compile and run the project, and if all looks well you should see the sample banks we added earlier!
Where to Go From Here?
Here’s the sample code for the project so far.
So far so good – except we’re missing the actual data from the failed banks. So next in the tutorial series we’ll cover how to preload/import existing data!