Core Data
Relevant resources
Documentation
Books
Video
Podcasts
Web
Sample code - Apple
Sample code - others
Sample code - class
SQLite and Core Data
For storing small amounts of data on the iPhone, you can use the user defaults, a plain text file, or an NSDictionary serialized to a property list. Anything more than trivial data storage needs to use something more robust. On the iPhone, the primary databases used for storage are SQLite and Core Data.
That last statement is a little misleading, because Core Data uses SQLite for its backend data store in the most popular configuration. SQLite is an extremely efficient file-based database that has been embedded in many devices due to its high performance and liberal license. As mentioned, no separate server application is required to access an SQLite database, given that the database itself is just a single file.
SQLite can be accessed via a C-based API, and was the only option for high-performance data storage on iPhone OS 2.x. It required pretty low-level design of your databases and SQL queries. Several people designed object wrappers around these C statements, such as Gus Mueller's FMDB or the Omni Group's OmniDataObjects.
On OS 3.x, we have an even better alternative in Core Data. Core Data is much more than just an object layer on SQLite database access. From the Core Data Programming Guide:
The Core Data framework provides generalized and automated solutions to common tasks associated with object life-cycle and object graph management, including persistence.
What this translates into is that Core Data lets you deal with your data as objects which link to other objects, without even thinking about how they are saved to disk. It manages these links for you, for example by removing a "parent" link for an object that has been removed from a matching "children" link in another object. This is what is meant by "object graph management", the maintenance of all of these linkages. As we'll discuss later, this object graph management extends to handling undo and redo for you.
On the iPhone, three different store types are available for persisting these objects to disk. They are SQLite, in-memory, and binary stores (the Mac also supports XML and custom atomic store types). By far the most common persistent store type is SQLite, because it lets you read only the data that you want from a file or only write the data that have changed, not the whole file each time. However, you as the developer don't need to know anything about what database is backing your Core Data model, because the framework handles all that for you.
By using the graphical tools provided for you for laying out your data model, you can avoid having to write the huge amount of code required to read and write your data model to disk, as well as maintain it within your application.
As a note, Aaron Hillegass recently unveiled a third option in his BNRPersistence framework, which is intended to be an ultra-high-performance data storage framework, but I can't comment on this new framework.
Performance advantages of Core Data
For all that Core Data does for you behind the scenes, you would expect that it adds significant overhead to reading data from and writing data to an SQLite database, hampering performance. Counterintuitively, it can actually make your applications faster and use less memory. It is for this reason that Apple has replaced the SQLite backend for its on-device applications (Contacts, etc.) with Core Data in iPhone OS 3.x.
Core Data uses advanced caching and optimized queries to extract the most performance out an SQLite database backend. Apple has demonstrated examples of where a default Core Data application beats even hand-tuned SQLite C code in terms of speed. Core Data is a mature framework, despite appearing on the iPhone only recently, and as Apple improves it further so will the performance of your applications using it.
Core Data lazy-loads data from disk only when needed. On the iPhone, this can save memory and lead to a faster startup time. When an object or attribute is loaded from disk, it is said to be "faulted" into memory. If you are curious about when this happens in your application, you can use the Core Data Cache Misses and Core Data Faults instruments in Instruments (which we will cover later when talking about performance tuning).
The Core Data stack
To get Core Data up and connected to your database in your application, you will need to set up what's called the Core Data stack. This stack consists of three components: the persistent store coordinator, the managed object model, and the managed object context.
The persistent store coordinator handles the connection to your database backend. In most cases, this will be an SQLite database that is either accessed or created upon initialization of this coordinator. To create a persistent store coordinator, you would use code like the following:
NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"scratchpad.sqlite"]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
nil];
scratchpadPersistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: self.overallManagedObjectModel];
NSError *error = nil;
if (![scratchpadPersistentStoreCoordinator ad configuration:nil URL:storeUrl options:options error:&error])
{
// Handle error
}
This creates a persistent store coordinator that is associated with the SQLite database scratchpad.sqlite. First, a path to the database file is generated. Then, the options for this persistent store are configured. In this case, we're going to be using the new lightweight migration capability introduced in iPhone OS 3.0 and Snow Leopard, where small changes to the Core Data model can be transparently migrated behind the scenes for you.
Finally, we create the persistent store coordinator, associate the database on disk with it, and specify the store type as being an SQLite database.
You might notice that we referred to a managed object model when creating the persistent store. This managed object model is created based on the model you designed in Xcode. The following is code to do this:
overallManagedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
This simply merges all of the separate data models you have defined in your application into one managed object model. We will refer back to this model when creating new objects based on entities defined in the model, or when using stored predicates.
The last component of the Core Data stack is the managed object context. This is set up using code like the following:
scratchpadContext = [[NSManagedObjectContext alloc] init];
[scratchpadContext setPersistentStoreCoordinator: coordinator];
The Core Data stack is typically constructed in some shared location, like the application delegate. My personal preference is to create a singleton which manages access to Core Data throughout the application. That way, you can access elements of the stack by doing something like:
[[MyDatabaseController sharedDatabaseController] overallManagedObjectModel]
In addition to using Core Data for read-write persistent stores, you can also ship prepopulated databases with read-only content in your application bundle (we'll discuss how to generate these later). This might be useful for supplying item data for a role-playing game, a lookup table for unit conversions, or any number of other purposes. To use such a database, you might want to let Core Data know that this persistent store will be read only by altering the options used to include the NSReadOnlyPersistentStoreOption:
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
[NSNumber numberWithBool:YES], NSReadOnlyPersistentStoreOption,
nil];
Designing a Core Data model
Core Data helps you to save a lot of code. It starts doing this by having you define your data model graphically within Xcode. Once you've created your project using a Core-Data-supporting template or added a new data model to your project manually, you can open it up into Xcode's graphical model editor. In this editor, you will see a schematic in the lower portion of the window, and a browser at the top. It is from here that you will lay out your data model.
Core Data uses unique terminology to describe the data model. Instead of objects, you define entities, which describe how data will be organized in Core Data. These entities have attributes, which are the individual data elements within the entity (analogous to properties in normal objects), and relationships, which are links between one or more entities (similar to pointers to objects, or arrays of pointers to objects). Entities can inherit from one another, similar to objects. However, be careful of this, because all entities that inherit from one root entity are all stored in the same table in SQLite, which might lead to wasted space and reduced data access performance.
Attributes can be of several different types: Integer (NSNumber), Double (NSNumber), Float (NSNumber), String (NSString), Boolean (NSNumber), Date (NSDate), Binary data (NSData), Decimal (NSDecimalNumber-based, not a normal floating point number), and Transformable. Each of these data types are standard Cocoa classes, with the exception of transformable attributes, which we will describe in a bit. You can set a default value for each attribute, which will be set to the attribute when a new object is created based on this entity. Other qualifiers can be set for each attribute, which must be satisfied in order for the entity to be validated.
Relationships can be to-one, to-many, or many-to-many. You add a relationship to your entity and can set whether or not the outbound link is to-one or to-many. It is highly recommended that all relationships have an inverse, so that the integrity of the object graph can be maintained. For example, if you have a children relationship that is to-many for a particular entity type, you will want to also create a parent relationship in the target entity type that is to-one and make both relationships the inverse of the other.
You can set rules for how deletion operations propagate through relationships by setting delete rules in your data model. These rules are No Action, Nullify, Cascade, and Deny. With No Action selected, you need to manage what happens to a relationship yourself. Nullify simply means that the inverse relationship to a deleted object is set to nil. Cascade means that if you delete an object, objects at the other end of the relationship will be deleted as well. You can delete an entire hierarchy of objects in one operation using this. Finally, Deny will prevent you from deleting the original object if something is set in this relationship.
Due to the way that Core Data stores objects in the SQLite database, to-many relationships are not ordered in any way. When you access them as properties, they are NSSets instead of NSArrays. Normally this is not a problem, but if you are creating an application that has user-ordered playlists, or something similar, you might need a way to add order to these relationships. There are a couple of ways of doing this. You could create an attribute in the members of this relationship that acts as a numerical key for ordering, then sort on that key when retrieving objects. You could maintain a linked list, with to-one relationships among the members of this list for the previous object and next object. Finally, if you have no desire to maintain this yourself, you could use Brian Webster's implementation of ordered managed objects: http://www.fatcatsoftware.com/blog/2008/per-object-ordered-relationships-using-core-data .
Transformable attributes
If you have a data type that is not one of the standard types, you can transparently convert between it and an NSData instance for storage in the database. An example of this would be a UIImage that you might like to store in the database as a thumbnail image. There is no UIImage type in Core Data, so we will need to use a transformable property to convert it to and from NSData, which we can store in the database. To do this, we first create an attribute in our Core Data entity that has a type of Transformable.
Next, we'll need to create an NSValueTransformer subclass. The following code implements a transformer that takes a UIImage and stores it in the Core Data database as NSData holding a PNG representation of the image (so that we can also access this on the Mac) and vice versa:
+ (Class)transformedValueClass
{
return [NSData class];
}
+ (BOOL)allowsReverseTransformation
{
return YES;
}
- (id)transformedValue:(id)value
{
if (value == nil)
return nil;
// I pass in raw data when generating the image, save that directly to the database
if ([value isKindOfClass:[NSData class]])
return value;
return UIImagePNGRepresentation((UIImage *)value);
}
- (id)reverseTransformedValue:(id)value
{
return [UIImage imageWithData:(NSData *)value];
}
Once the value transformer has been constructed, go back to the transformable attribute in your data model and set its value transformer to the name of this class. If you create a custom NSManagedObject subclass for this entity type, you can now create a property with the same name as your attribute, only with a UIImage* type instead of NSData*. The transformation to and from NSData will happen for you behind the scenes.
Predicates and fetching data
To pull in individual objects, or arrays of objects matching a certain pattern, you will need to use a fetch request. This fetch request will grab objects for a particular entity type, filtered by a predicate that you provide, and sorted by a sort descriptor. An example of this follows:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"DataItem" inManagedObjectContext:managedObjectContext]];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"title" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[sortDescriptor release];
[fetchRequest setSortDescriptors:sortDescriptors];
[sortDescriptors release];
NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:@"dateCreated", @"thumbnailImage", nil];
[fetchRequest setPropertiesToFetch:propertiesToFetch];
[propertiesToFetch release];
NSPredicate *pred = [NSPredicate predicateWithFormat:@"category = nil"];
[fetchRequest setPredicate:pred];
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
First, we construct the fetch request and associate an entity with it (DataItem in this case) from a particular managed object context. We then set the batch size of results that will be fetched into memory at one time. This one line of code can lead to a huge improvement in performance within your application, as it causes data to be loaded and unloaded in chunks only as needed. In this case, the first 20 items will be loaded, and others paged when requested, rather than all possible results. This can be much faster and use much less memory.
Next, we configure a sort descriptor to place the results from our fetch request in ascending order based on the title attribute of the entity. A fetch request can take an array of sort descriptors in order to sort based on multiple attributes.
As a further optimization, we can specify only certain attributes to be faulted into memory with the objects we are fetching. For a table view, you may only use one or two attributes of an entity to display a table cell, with the remainder needed only if a row is selected. Therefore, you may wish to only load only the immediately relevant attributes in order to speed up your fetch request and minimize memory usage.
We can specify a predicate to filter the objects for the selected entity type. In this case, we're finding only those objects whose category relationship is not set (nil).
Finally, we run this fetch request against our managed object context and get back an NSArray of results. If nothing matches our criteria, the array will be nil. Otherwise, it will contain a sorted array of DataItem objects whose category relationship is not set.
If you just want to obtain a count of the number of entities that would be returned from a fetch request, you can use code like the following:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:entityName inManagedObjectContext:moc]];
NSError *err;
NSUInteger count = [moc countForFetchRequest:request error:&err];
if(count == NSNotFound)
{
//Handle error
}
[request release];
For a full breakdown of all the capabilities of NSPredicates, I highly recommend reading the Predicate Programming Guide.
Loading data from disk
In addition to manual fetch requests, Core Data loads data from disk as necessary when relationships are accessed. Any entity that hasn't been used yet in your application exists as a fault, a virtual object that hasn't yet been loaded from disk. The first time you access a property of that entity, a fault is fired and the entity is loaded from disk. This lazy-loading has significant performance advantages, but if you are performing multiple individual faults you may experience worse performance than one single fetch of multiple entities.
To observe faults firing in your application, you can use the Core Data Faults instrument in Instruments to monitor data access in your application. However, this only works in the Simulator, not on the device, due to the lack of DTrace in the current iPhone OS.
Creating and deleting objects
Creating an NSManagedObject for insertion into Core Data is a little different than allocating and initializing a normal object. Because new objects being created in Core Data are based on entities, which define their data structure, you need to identify the entity to use for the new object. Each object must also be asssociated with a managed object context, into which it is inserted on creation. For example, the following code will create a new object based on a DataItem entity:
NSEntityDescription *equationEntityDescription = [NSEntityDescription entityForName:@"DataItem" inManagedObjectContext:managedObjectContext];
NSManagedObject *newDataItem = [[NSManagedObject alloc] initWithEntity:equationEntityDescription insertIntoManagedObjectContext:managedObjectContext];
You delete an object by removing it from the context:
[managedObjectContext deleteObject:objectToDelete];
Note that this will not immediately remove the object from the on-disk database. In fact, the object itself can be restored using undo / redo.
Saving data to disk
Saving the data to disk is pretty simple, and just involves using the -save: method on a managed object context:
NSError *error = nil;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
NSLog(@"Save error %@, %@", error, [error userInfo]);
for (id errorObject in [[error userInfo] valueForKey:NSDetailedErrorsKey])
{
NSLog(@"Detailed error: %@", errorObject);
}
error = nil;
}
}
In this example, we check to make sure that the save completed successfully if there were changes to save to disk. Any errors are captured and logged out for debugging purposes.
Usually, code like this is triggered by -applicationWillTerminate: to save data on your way out of the application, but you may wish to save changes at other locations to protect against data loss during a crash or other unnatural program interruption.
NSFetchedResultsController and table views
On the iPhone, it is common to represent data by using a table view. To aid this, Apple has provided a very handy helper class called NSFetchedResultsController. NSFetchedResultsController acts as a go-between for a UITableView and Core Data. You assign it an NSFetchRequest, which it uses to grab the objects it will present to the table view, and it does the rest for you. When used with a batch size set on the NSFetchRequest, this can be an extremely powerful class, as it will only fetch objects that are currently displayed onscreen in your table view, loading and unloading data as you scroll through the list. This way, you never have to worry about memory consumption or caching of your row data objects.
If you create a new project from the Navigation-based Application template and check the Use Core Data for storage box, you will end up with a working example of NSFetchedResultsController managing a table view for you. Otherwise, there is just some boilerplate code you need to implement in your UITableViewController subclass to make this all work.
First, you'll need to create an NSFetchedResultsController using an NSFetchRequest. We saw how to create such a fetch request above, and that same code used to manually query for objects meeting certain conditions, sorted in a certain way, can be fed right into a new NSFetchedResultsController.
Then you'll want to implement the following code:
- (void)viewDidLoad
{
NSError *error = nil;
if (![fetchedResultsController performFetch:&error])
{
NSLog(@"Error fetching request: %@", [error localizedDescription]);
}
[super viewDidLoad];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// This is Apple's workaround to fix for 3.0's inconsistent behavior when dealing with deleting the last row
NSUInteger count = [[fetchedResultsController sections] count];
if (count == 0)
{
count = 1;
}
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// This is Apple's workaround to fix for 3.0's inconsistent behavior when dealing with deleting the last row
NSArray *sections = [fetchedResultsController sections];
NSUInteger count = 0;
if ([sections count])
{
id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
count = [sectionInfo numberOfObjects];
}
return count;
}
as well as the following delegate methods to handle changes in your data model other than those on this table:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
}; break;
case NSFetchedResultsChangeDelete:
{
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
}; break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}; break;
case NSFetchedResultsChangeDelete:
{
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}; break;
case NSFetchedResultsChangeUpdate:
{
[self updateCell:[self.tableView cellForRowAtIndexPath:indexPath] fromEquation:[fetchedResultsController objectAtIndexPath:indexPath]];
}; break;
case NSFetchedResultsChangeMove:
{
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
}; break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
Within -tableView:cellForRowAtIndexPath: or other methods that ask you to respond to something happening to a row, you simply need to use the passed-in index path and NSFetchedResultsController's -objectAtIndexPath: method to grab the appropriate object to show data for or respond to some action on.
For example, to handle deletion of a row, you might use code like the following:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[fetchedResultsController.managedObjectContext deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
}
}
Custom managed object subclasses
By default, all entities in your Core Data model are handled as instances of NSManagedObject. You can do a lot with these objects, but you might want to have custom classes to perform some custom calculations, transform data attributes transparently, or just implement properties that you can more easily access. There are a few ways to subclass NSManagedObject.
First, you can add a new NSManagedObject subclass by choosing File | New File and selecting Managed Object Class from the iPhone files list. You will be asked which entity to generate a subclass for, as well as whether or not to create accessors, properties, and validation methods for it. When done, you will have a prepopulated template subclass from which you can start your customization process.
You can also start with a blank subclass of NSManagedObject and manually add in the code for your entity's attributes by going to the entity, selecting its attributes, and right-clicking to bring up a menu where you can choose to Copy Obj-C 2.0 Method Declarations to Clipboard or Copy Obj-C 2.0 Method Implementations to Clipboard. You can then paste this generated code for the selected attributes into your subclass to create the appropriate properties.
Finally, Wolf Rentzsch has created a great tool for automating the maintenance of these custom NSManagedObject subclasses in mogenerator.
Once a custom subclass has been created for an entity, you will want to make sure that it is associated with that entity by going to your data model and changing its class from NSManagedObject to whatever you called your custom subclass.
You'll notice that most of the properties you define in these subclasses don't use @synthesize, but rather @dynamic. This is one case where we let the property implementations be generated at runtime, rather than by the compiler.
NSManagedObject is handled a little differently than other Cocoa objects, so you need to be aware of a few things when subclassing it. Rather than using -init or other places to initialize values in your object, you need to override -awakeFromInsert and -awakeFromFetch:
- (void)awakeFromInsert
{
[super awakeFromInsert];
// Your custom initialization code here
}
- (void)awakeFromFetch
{
[super awakeFromFetch];
// Your custom initialization code here
}
-awakeFromInsert will be called on the first time an object is inserted into Core Data and -awakeFromFetch is called every time an object is fetched from the database.
Similarly, any cleanup code should not go in -dealloc or the like, but in -willTurnIntoFault:
- (void)willTurnIntoFault
{
[super willTurnIntoFault];
// Your custom cleanup code here
}
When overriding accessors, you will need to access the primitive values for an attribute. By accessing a primitive value, you bypass the normal change notification system of Core Data and thus prevent an infinite recursion. The following is an example of an overridden setter method:
- (void)setNumericalValue:(NSDecimalNumber *)value
{
[self willChangeValueForKey:@"numericalValue"];
[self setPrimitiveNumericalValue:value];
[self doSomething];
[self didChangeValueForKey:@"numericalValue"];
}
We manually notify about the change of a particular value, set the primitive value, and do our custom class method. In order to avoid a compiler warning for -setPrimitiveNumericalValue:, you may need to create a category that defines the interface of this method in your subclass interface.
Model versioning and migration
After your application has been out in the wild for a time, there may come a point when you need to change the structure of your data model. To do this without losing your users' databases, you will need to implement a means of migrating from an old database version to a new one. Fortunately, for iPhone OS 3.0 and Snow Leopard, Apple introduced a lightweight migration process for data models where only trivial changes have occurred. For more elaborate changes in the data model, you will need to set up the migration process yourself.
I mentioned lightweight migration above when we talked about the options for setting up a persistent store. In the examples given, the options to enable lightweight migration were turned on. If this has been enabled, Core Data will transparently migrate a database using an older data model to your latest version with no additional code required.
However, for this to work you will need to create a new version of your data model which is different from your previous data model version so that Core Data will know what to migrate to. To do this, pull up your data model in Xcode and choose the menu item Design | Data Model | Add Model Version. If this is the first time you've added a version to your data model, your .xcdatamodel file will be moved into a directory that has the .xcdatamodeld extension. This directory will host copies of your data model, one for each version you've created. You can set which one to use as your latest data model by selecting the menu item Design | Data Model | Set Current Version. Keep a copy of the data model around for each version of your application's data model that is out in the field.
Lightweight migration will handle simple changes, like the addition of an attribute, changing whether an attribute is optional, or providing a default value for an attribute. Renaming an attribute can also me migrated automatically if you provide a renaming identifier in your Xcode data model for the attribute. This option can be found on the Configurations tab in the far right of the browser area when an attribute is selected.
For more complex cases of data model migration, you will need to create a migration model. For more on this, see the Core Data Model Versioning and Data Migration Programming Guide.
Undo / redo
Core Data can handle all of the behind-the-scenes work for you in order to implement undo and redo within your application, even when you are making complex changes to your data model. You get this functionality for close to free by using the framework to manage your data.
To enable undo / redo, you first need to set up an NSUndoManager for your managed object context using code like the following:
NSUndoManager *contextUndoManager = [[NSUndoManager alloc] init];
[contextUndoManager setLevelsOfUndo:10];
[managedObjectContext setUndoManager:contextUndoManager];
[contextUndoManager release];
This code would be located near where you created your managed object context. Note that you can set the levels of undo for your application. More levels can lead to greater memory usage, so only use as many as you think is reasonable for your application.
Once an undo manager is provided for your context, you need to enable the default gesture for undo on the iPhone, a shake of the device. To let your application handle this gesture automatically, place the following code within the -applicationDidFinishLaunching: method in your application delegate:
application.applicationSupportsShakeToEdit = YES;
Finally, you will need to set up each view controller that will be capable of handling the shake gesture for undo. These view controllers will need to report back the undo manager to use for that controller by overriding the -undoManager method:
- (NSUndoManager *)undoManager;
{
return [[[MATCDatabaseController sharedDatabaseController] scratchpadContext] undoManager];
}
The view controllers will also need to be able to become the first responder to handle gestures, so the following method is needed:
- (BOOL)canBecomeFirstResponder
{
return YES;
}
Finally, the view controller will need to become the first responder when it appears onscreen. This can be done by calling [self becomeFirstResponder] in -loadView or -viewDidLoad, but I have found that view controllers which appear onscreen immediately after launch need to have this message delayed a bit in order for it to work:
[self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.3];
With all this in place, you should get automatic undo and redo support, with a nice animated menu. That's very little code for some powerful functionality.
Core Data performs undo actions based on individual model changes that you cause. For example, if you change a value for an attribute, and then change another value for another attribute, each of those changes are single events that are undone separately. Likewise, deleting an object, which causes a cascading deletion of all its children and their children, is handled as a single undoable action.
If you wish to group a series of actions together so that they are undone / redone in one step, you can place them within code like the following:
[[[[MATCDatabaseController sharedDatabaseController] managedObjectContext] undoManager] beginUndoGrouping];
// Perform your data model edits here
[[[MATCDatabaseController sharedDatabaseController] managedObjectContext] processPendingChanges];
[[[[MATCDatabaseController sharedDatabaseController] managedObjectContext] undoManager] endUndoGrouping];
Every data model change between these bracketing commands will be treated as a single action for undo / redo purposes. The call to -processPendingChanges at the end of the group forces all changes to the data model that might have been performed later to be done at that instant, ensuring that they are included in the undo grouping.
Likewise, you can block certain data model changes from being handled by the undo manager. Perhaps you have a cached value that you store in the database where you don't want users to be able to undo its recalculation. Same as with grouped actions, you just need to bracket those changes in code like the following:
[[[[MATCDatabaseController sharedDatabaseController] managedObjectContext] undoManager] disableUndoRegistration];
// Perform your data model edits here
[[[MATCDatabaseController sharedDatabaseController] managedObjectContext] processPendingChanges];
[[[[MATCDatabaseController sharedDatabaseController] managedObjectContext] undoManager]enableUndoRegistration];
Once again, we need to process all pending changes to make sure that they are all ignored by the undo manager.
Populating a database on the Mac
Core Data is a mature framework, having existed on the Mac since Tiger. When it was brought across to the iPhone, it retained the same internal structure for SQLite persistent stores. This makes it relatively easy to write a simple Mac application which can prepopulate an iPhone application's Core Data database. You might want to do this in order to start the application out with some initial data, or to bundle read-only data with the application.
Core Data Editor is a third-party application that can load in your data model (the compiled data model with the .mom extension in your compiled application bundle). It provides one quick way to edit the contents of a Core Data database.
It's very easy to create a skeleton application to edit your Core Data database. First, create a new Cocoa application that uses Core Data for storage. Replace the data model in that application with the exact one that you have created for your iPhone application (by reference to the file in your iPhone project directory if you'd like to keep it up to date). Bring up the main window of the application in Interface Builder. Go to your Core Data model, hold down Option, and click and drag an entity from your model onto the Interface Builder window. A dialog will appear that lets you automatically generate an interface for entering and editing new entities of this type in your model. You can select how to present this and what fields to edit.
This lightweight interface will give you the capability of quickly editing your data model, but you may wish to improve upon it to create a more friendly data entry client. This is also a great way to get a start on a Mac client for your iPhone application.
To bring this database across to the iPhone, you can place it as a resource in your iPhone application project and either work with it as a read-only database in the application bundle or copy it over to the application's documents directory on first launch.
When designing applications to prepopulate a Core Data database, many people are tempted to write directly to the SQLite database used on the backend. Do not do this, because a specific, undocumented structure is used for these databases and if you don't get it exactly right your Core Data database will not load correctly. Apple may also change the internal structure of this database at any time, so treat the whole thing as a black box when it comes to writing data out for use in a Core Data application.
Debugging Core Data databases
Even though I just mentioned that you should not rely on the internal structure of a Core Data SQLite database for editing, it still can be very useful to peek inside that database for debugging purposes. Any SQLite client can read these databases (my personal favorite is Base). It can be extremely useful to see if proper attributes or linkages between objects are being written out to disk, particularly if something is going wrong in your data saving / loading routines. You can also use Core Data Editor for this, but I like to look at the direct SQLite data store myself.
There are few tables within your database that are of interest. The first is Z_PRIMARYKEY. This stores within it the names of your various entity types, as well as the numerical keys used to identify them within other tables. This table also has keys for parents of each entity, as you defined them in your Xcode data model.
The remaining tables of interest all start with Z and end with an all-caps version of an entity's name. These tables hold each of your objects that are based on that specific entity. Note that entities which inherit from other entities are all placed within one master table. The columns within this table all correspond to the attributes and relationships that you've specified for that type of entity. You'll be able to read strings, numbers, and other simple types here to verify that they are indeed being set properly. Relationships have columns for both the key of the object at the other end of the relationship and its type. You can figure out what type of object is referred to by looking up the entity type key in the previously mentioned Z_PRIMARYKEY, then find out which object it is by checking the appropriate table for an object with a matching Z_PK value for the object key.
-Multithreading and Core Data
-Saved predicates in data model
More iPhone 3 Development: Tackling iPhone SDK 3 by Dave Mark and Jeff LaMarche