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.
Key Takeaways
- CoreData is a powerful framework provided by Apple for iOS and macOS applications that simplifies data management and ensures data consistency, crucial for creating reliable and efficient applications.
- CoreData is not just a database; it’s a complete data management solution that handles various tasks such as memory management, object graph management, and data validation.
- CoreData supports iCloud, allowing for seamless data syncing across multiple devices. However, setting up iCloud syncing with CoreData can be complex and requires careful handling of merge conflicts.
- CoreData provides built-in support for data migration, allowing for lightweight migration for simple changes to your data model, or manual migration for more complex changes. It also supports versioning, which allows you to keep multiple versions of your data model and migrate data between them.
- While CoreData is a powerful framework, it does have limitations. It’s not suitable for storing large binary data like images and videos, doesn’t support full-text search or complex queries like SQL, and while it does support multi-threading, it can be complex to set up and manage.
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.
Click on Orny.xcdatamodeld
(this is the data model definition file).
You should see one of the following two screens:
You can switch between them using ‘Editor Style’ in the bottom right.
To add an entity, click the ‘Add Entity’ button.
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.
Click the ‘+’ button under Attributes to add some attributes – name, filename, and text_description.
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.
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’.
Hit ‘Next’, and select the ‘Orny’ data model.
Hit ‘Next’, select ‘Species’
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:
- Manually creating the .sqlite file
- Making your app save a copy of the empty database, then modifying that to become your default database
- 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
.
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’.
In SQLite Browser, hit the ‘Browse Data’ tab, and select ‘ZSPECIES’ from the table drop-down.
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.
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.
- iOS Development Basics with Xcode 4
- iOS Apps with Tasty UI
- An Interactive Orny
- Managing Information with CoreData
- …
Frequently Asked Questions about Managing Information with CoreData
What is CoreData and why is it important in iOS development?
CoreData is a powerful framework provided by Apple for iOS and macOS applications. It’s essentially a persistence layer that allows developers to manage the model layer objects in their applications. CoreData can save data to disk, manage object graphs, track changes, and handle undo and redo operations. It’s important in iOS development because it simplifies data management and ensures data consistency, which is crucial for creating reliable and efficient applications.
How does CoreData differ from other data management frameworks?
CoreData stands out from other data management frameworks due to its robustness and versatility. It’s not just a database; it’s a complete data management solution that handles various tasks such as memory management, object graph management, and data validation. CoreData also supports iCloud, allowing for seamless data syncing across multiple devices.
How do I fix the “Cannot find NSFetchRequest in scope” error?
This error typically occurs when you’re trying to use NSFetchRequest without importing CoreData. To fix this, you need to import CoreData at the top of your Swift file. Here’s how you do it:import CoreData
After importing CoreData, you should be able to use NSFetchRequest without any issues.
What is NSFetchRequest and how is it used in CoreData?
NSFetchRequest is a class in CoreData that allows you to describe the data you want to fetch from your persistent store. You can specify the entity, sort descriptors, and predicates to filter the results. NSFetchRequest is used to retrieve data from your CoreData store, making it a crucial part of data fetching operations.
How can I improve the performance of my CoreData application?
There are several ways to improve the performance of your CoreData application. First, use batch fetching and batch updates to minimize the number of trips to the persistent store. Second, use appropriate fetch predicates to narrow down the data you’re fetching. Third, avoid using too many relationships as they can slow down fetch operations. Lastly, use background contexts for long-running operations to avoid blocking the main thread.
How do I handle relationships in CoreData?
CoreData allows you to define relationships between entities, similar to how you would in a relational database. You can create one-to-one, one-to-many, and many-to-many relationships. When defining a relationship, you also need to specify the delete rule, which determines how CoreData should handle the related objects when the source object is deleted.
Can I use CoreData with iCloud?
Yes, CoreData supports iCloud, allowing you to sync data across multiple devices. However, setting up iCloud syncing with CoreData can be complex and requires careful handling of merge conflicts.
How do I migrate data in CoreData?
CoreData provides built-in support for data migration. You can perform lightweight migration for simple changes to your data model, or manual migration for more complex changes. CoreData also supports versioning, which allows you to keep multiple versions of your data model and migrate data between them.
What are the limitations of CoreData?
While CoreData is a powerful framework, it does have some limitations. It’s not suitable for storing large binary data like images and videos. It also doesn’t support full-text search or complex queries like SQL. Lastly, while CoreData does support multi-threading, it can be complex to set up and manage.
How do I debug issues in CoreData?
Debugging CoreData issues can be challenging due to its complexity. However, there are several tools and techniques you can use. Xcode provides a CoreData model editor that allows you to visually inspect your data model. You can also use NSManagedObject’s valueForKey:
and setValue:forKey:
methods to inspect and modify your data at runtime. Additionally, you can enable CoreData’s debug logging to get detailed information about its operations.
Andy is a software developer who regularly uses PHP, Ruby, and Objective-C to build for the Web and mobile web. His passions are teaching and enabling people, and building new things. In addition to holding down a secretive day job, he runs OiOi Studios by night.