Chapter 5. Introducing Core Data

WHAT'S IN THIS CHAPTER?

  • Describing what the Core Data API can do for you

  • Understanding the various objects that make up the Core Data API

  • Understanding how the template code works to configure your application to use Core Data

  • Creating a simple application that uses Core Data to maintain application state

Now that you have completed the first part of the book, you should be comfortable using an SQLite database and implementing views into your data with the UITableView control. You have learned to build data-driven applications for the iPhone that can efficiently access large amounts of data stored in an SQLite database and display that data with your own, highly customized, tables.

Up until this point, you have not looked at how to store the data that users create on the device. For instance, if you wanted to create a task manager, you would need to be able to save the user-created tasks. You could use SQLite to INSERT the data, now that you know how to execute arbitrary SQL statements against the database. However, there is an API designed specifically for storing the objects in your application model: Core Data.

In this chapter, you learn about the architecture of the Core Data API and the classes that you use to build a Core Data application. You will walk through a Core Data template application to learn how to implement the architecture in code. Then you will learn how to build a simple application that uses Core Data for storage.

This chapter prepares you for the next few chapters where you dive deeply into the Core Data tools and API.

THE BASICS OF CORE DATA

Core Data is a set of APIs designed to simplify the persistence of data objects. Sometimes you will hear people refer to Core Data as an object persistence framework or an object graph manager. Core Data provides a framework for saving your model objects and retrieving them later. Core Data also manages changes to your object model, provides undo support, and ensures the consistency of relationships between your model objects. All of these features help to free you from having to write the code to implement this functionality. In essence, Core Data simplifies the creation of the Model part of the Model-View-Controller architecture.

The foundation of the Core Data tool set is a code-based API used to manipulate your data objects in code. However, the tool set also includes a graphical data modeler that you can use to define your model objects. The modeler allows you to define your data objects, their attributes, and their relationships with the other objects in your application. You can even specify constraints and simple validation rules inside the graphical tool. You explore the data modeling tool and all of its uses in the next chapter.

The graphical modeler simplifies creation of the model in the same way that Interface Builder simplifies the creation of the view. Using Core Data and Interface Builder, you can quickly build the Model and View components of the MVC architecture, leaving only the controller business logic left to code. This can significantly reduce the development time of your projects.

In addition to its ease of use, Core Data provides some important performance enhancements as well. Core Data can use SQLite as its backing data store. This provides a Core Data with high performance query engine. When compared to searching and sorting through flat data files or plists, Core Data is the clear choice for speed. Additionally, the API is able to conserve memory by retrieving only the data that you need at any specific time. For example, if you have two related entities, Core Data will not retrieve the child entities until you ask for them. This conserves memory, which is a scarce resource on mobile devices. You learn more about this functionality, called faulting, in the chapters to come.

Core Data provides many of the functions that you would expect to find when dealing with data objects. Specifically, you can filter your data using predicates with the NSPredicate class, and sort your data using the NSSortDescriptor class. I touch on these classes here and get into much finer detail in Chapter 6.

THE CORE DATA ARCHITECTURE

To understand how to use Core Data, it helps to have an understanding of the underlying architecture. While you will not be accessing most of the objects contained in the API directly, you will be much more proficient if you understand how Core Data works.

The Core Data Stack

The design of Core Data is a stack structure, as shown in Figure 5-1. The key components are the data store, the Persistent Store Coordinator, the Managed Object Model, and the Managed Object Context. While I feel that it is important to understand what is going on behind the scenes, try not to get confused by the terminology introduced in this section. It may seem overwhelming now, but as you work through the template and the sample in this chapter, the details of how you use Core Data should become clear.

The Core Data stack

Figure 5.1. The Core Data stack

The Data Store

The data store is the file or group of files that hold your data. This is the actual file written to the disk when the save message is sent to Core Data. Typically, in a mobile application, only one data store file is used. However, it is possible to use a group of files as the data store.

The data store can be a binary data file, an SQLite database, or an in-memory data file, depending on the parameters used when creating the data store. You will see how to specify the storage type for your data in the example at the end of the chapter.

As the developer, you will never directly access the data store. The Persistent Store Coordinator abstracts away access to the data file. In addition, you do not need to concern yourself with the data store implementation. Simply consider it a file that holds all of your data.

The Persistent Store Coordinator

This Persistent Store Coordinator acts as a mediator between the managed object context and the data store. The coordinator takes requests for data from the context and forwards them to the appropriate data store. The coordinator also allows the context to access one or more data stores as if they were one. Finally, the coordinator associates a data store with a Managed Object Model. The Persistent Store Coordinator is an instance of the NSPersistentStoreCoordinator class.

Warning

You need to be aware that the NSPersistentStoreCoordinator class is not thread safe. Therefore, if you plan to access a data store simultaneously across multiple threads, you have to either create a coordinator for each thread or lock and unlock the single coordinator manually.

The Managed Object Model

The Managed Object Model represents the data model schema. In code, the Managed Object Model is an instance of the NSManagedObjectModel class.

The model consists of a set of entities that define the data objects in your application. When designing your model, you specify the data entities that your application will deal with. You can specify attributes for these entities and define the relationships between them.

You typically create the model graphically using the Xcode data-modeling tool although it is possible to define the model in code. You can think of the managed object model like the Entity-Relationship diagram that you would create when designing a database.

The data model should define each data object used in your application. The Persistent Store Coordinator uses the model to create Managed Objects according to conventions from the entities defined in the model. The coordinator also maps the entities in the model into the physical data store file that Core Data writes to disk.

You will rarely access the object model through your code. If the need arises, you can use the NSManagedObjectModel class. You access the Managed Objects created from the model entities using the Managed Object Context.

The Managed Object Context

The Managed Object Context, also referred to as the context, provides the main interface that you will use to access your managed data objects. The managed object context is an instance of the NSManagedObjectContext class.

You use the context to hold all of your managed data objects. Your managed data objects are either instances or subclasses of the NSManagedObject class. The name Managed Object makes sense as the Managed Object Context manages all of these objects.

You can think of the context as the sandbox that holds all of your application data. You can add objects to the context, delete them, and modify them in memory. Then, when you are ready, you can tell the context to commit its current state to disk. Behind the scenes, the context uses the Persistent Store Coordinator to write your data out to the data store on disk. The context uses the object model to ensure that your data is in a consistent state with respect to your defined relationships, constraints, and validation rules before committing it to disk.

You make fetch requests against the context to fetch data from the data store back into the context. You fetch data into Managed Objects that you use to manipulate and display your data. Fetch requests are similar to SQL SELECT statements. When creating a fetch request, you can provide a predicate to filter your data such as the SQL WHERE clause. You also provide a sort array that functions like the SQL ORDER BY clause.

SQLite and Core Data

In Chapter 2, you learned how to use SQLite as the database engine for your application. Now that you have learned about Core Data, you may be wondering how the two are related.

SQLite is a library that provides a relational database implementation used on the iPhone. Core Data can use SQLite as the on-disk data store to persist your data. Core Data can also use a proprietary binary data file for the data store. However, I would not generally recommend this because the binary format requires that your entire object graph be loaded into memory as opposed to using the SQLite format, which allows parts of the object graph to be loaded as needed.

While you learned how to view and modify the schema and data in an SQLite database, you should never attempt to manually modify the schema or data in a Core Data SQLite data store. Core Data requires that the data in the database be stored in a specific way in order to function. You should feel free to browse the schema and data if you care to, but you should never attempt to modify either.

USING CORE DATA: A SIMPLE TASK MANAGER

Now that you are familiar with the terminology and the Core Data API architecture, I am going to walk through a simple example application so that you can get your feet wet with using Core Data. You will first take a look at everything that you get "for free" when you start a new application and tell Xcode that you want to use Core Data as the backing data store. Then, you will customize the template-generated code to create an application to keep track of a very simple task list.

You may feel overwhelmed by all of the information that you learned in the previous section. You should take two things away from the previous section. First is that most, if not all, of your interaction with Core Data in your code will be through the Managed Object Context and Managed Objects. The other is that you will define your data model graphically using the tool provided in Xcode.

Creating the Project

The application that you are going to create is a simple task manager. It will allow you to enter new tasks and remove completed tasks. When you create a task, it will be time-stamped. The application will sort the tasks based on the timestamp with the newest tasks at the top of the list. Figure 5-2 shows the application in action.

The completed Tasks application

Figure 5.2. The completed Tasks application

To start the project, open Xcode and create a new Navigation-based Application from the new project template. Make sure that you select the option to "Use Core Data for storage" in the new project window. Call the new project "Tasks."

After you have created the project, build and run it. You will see that you get quite a bit of functionality from the template without adding any code at all.

If you click on the plus button in the upper-right corner, time-stamp entries get added to your TableView. You can swipe across an entry and click the Delete button to delete individual timestamps. You can also click the Edit button to go into editing mode where you can delete entries by clicking the icon to the left of a timestamp and then clicking the Delete button.

If you click the Home button in the iPhone simulator and then click the icon on the home screen for your Tasks application, you will see that any timestamps that you did not delete are still there. Your application already includes the code required to persist your data.

Examining the Template Code

Before you get into modifying the code to create and display tasks, let's take a walk through the template code. This should provide you with some insight into how the application works along with showing you how to implement the Core Data architecture in code. Keep in mind that all of this code is auto-generated when you select the Navigation-based Application template and opt to use Core Data for storage.

Two classes are created by the template: RootViewController and AppDelegate. You create these same classes in any typical Navigation-based application. Additionally, if you open the Resources folder of your project, you will see a data model file that was also auto-generated called Tasks.xcdatamodel. Let's look at each of these files.

TasksAppDelegate

While you won't be modifying any of the code in the app delegate, it is instructive to look at the code. The code in the app delegate sets up the Core Data stack. Therefore, as you might imagine, there is code to create a Persistent Store Coordinator, Managed Object Model, and Managed Object Context.

The persistentStoreCoordinator getter method returns the Persistent Store Coordinator for the application. Here is the code:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (persistentStoreCoordinator != nil) {
        return persistentStoreCoordinator;
    }

    NSURL *storeUrl = [NSURL fileURLWithPath:
                       [[self applicationDocumentsDirectory]
                        stringByAppendingPathComponent: @"Tasks.sqlite"]];

    NSError *error = nil;
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel:[self managedObjectModel]];
    if (![persistentStoreCoordinator
          addPersistentStoreWithType:NSSQLiteStoreType
          configuration:nil URL:storeUrl
          options:nil
          error:&error]) {
        /*
         Replace this implementation with code to handle the error
         appropriately.

         abort() causes the application to generate a crash log and terminate.
         You should not use this function in a shipping application, although
         it may be useful during development. If it is not possible to recover
         from the error, display an alert panel that instructs the user to quit
         the application by pressing the Home button.

         Typical reasons for an error here include:
         * The persistent store is not accessible
         * The schema for the persistent store is incompatible with current
            managed object model
         Check the error message to determine what the actual problem was.
         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return persistentStoreCoordinator;
}
                                                                       
TasksAppDelegate

The code first determines if the Persistent Store Coordinator already exists. If it exists, the method returns it to you.

If the coordinator does not exist, the code must create it. In order to create the coordinator, you need to pass in a URL that points to the data store. The template uses the fileURLWithPath: method on the NSURL class to create the URL. You can see that the store name will be Tasks.sqlite.

Next, the code allocates an NSError object to hold the error data that the Persistent Store Coordinator generates if there is a problem configuring the coordinator.

The next line allocates and initializes the coordinator using the Managed Object Model. You will look at the managedObjectModel getter method in a moment. Remember that you use the Persistent Store Coordinator to mediate between the Managed Object Context, the Managed Object Model, and the data store. Therefore, it makes sense that the coordinator would need to have a reference to the model.

Now that the coordinator knows about the model, the code goes on to tell it about the data store. The next line of code adds the data store to the coordinator. You will notice that the type of the data store is NSSQLiteStoreType. This indicates that the template uses the SQLite backing data store. If you wanted to use the binary store, you would change this enumeration value to NSBinaryStoreType, and if you wanted to use the in-memory store, you would set the value to NSInMemoryStoreType. Remember that most often you will be using the SQLite backing store.

The rest of the code logs an error if there was a problem adding the SQLite store to the coordinator. If there was no error, the method returns the coordinator.

The getter function to return the Managed Object Model is very straightforward:

- (NSManagedObjectModel *)managedObjectModel {

    if (managedObjectModel != nil) {
        return managedObjectModel;
    }
    managedObjectModel = [[NSManagedObjectModel
                           mergedModelFromBundles:nil] retain];
    return managedObjectModel;
}
                                                         
TasksAppDelegate

If the model already exists, the getter method returns it. If not, the code creates a new Managed Object Model by merging all of the model files contained in the application bundle. The Tasks.xcdatamodel file in the Resources folder contains the object model. This file is included with the application bundle, so this method takes that file and uses it to create the NSManagedObjectModel object.

The last bit of interesting code in the App Delegate is the managedObjectContext getter method:

- (NSManagedObjectContext *) managedObjectContext {

    if (managedObjectContext != nil) {
        return managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator =
        [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator: coordinator];
    }
    return managedObjectContext;
}
                                                         
TasksAppDelegate

This method works similarly to the previous two. The first thing it does is check to see if the context already exists. If the context exists, the method returns it.

Next, the code gets a reference to the persistentStoreCoordinator. If you refer back to Figure 5-1, you will see that the Managed Object Context needs only a reference to the Persistent Store Coordinator.

If the code successfully gets a reference to the coordinator, it goes on to create the context. Then, the code sets the coordinator for the context and returns the context.

I hope that the way in which all of the objects in the Core Data stack fit together is becoming a bit clearer. Remember that you did not have to write any code to create the data store, the Persistent Store Coordinator, the Managed Object Model, or the Managed Object Context. The template code takes care of all of this for you automatically. You will see that the only interface that you need to deal with is that of the Managed Object Context.

The Data Model

Before you examine the RootViewController and how to use it to create your TableView using Core Data, let's look at the template Managed Object Model. If you open the Resources folder in Xcode and double-click on Tasks.xcdatamodel, you should see something similar to Figure 5-3.

The default Object Model

Figure 5.3. The default Object Model

The code template creates a default entity called "Event." The blue highlighting and resize handles indicate that the Event entity is selected. Click anywhere else in the diagram to de-select the Event entity and it will turn pink to indicate that it is no longer selected. Click on the Event entity again to select it.

There are three panes in the top of the data-modeling tool. From left to right they are the Entities pane, the Properties pane, and the Detail pane. The Entities pane provides a list of all of the entities in your model. The Properties pane lists the properties of the currently selected entity, and the Detail pane shows details related to whatever is currently selected, either an entity or property. The bottom portion of the tool displays a graphical representation of your model called the Diagram View. The next chapter provides more detail on using the data-modeling tool.

You can see in Figure 5-3 that the Event entity has an attribute called timeStamp. If you select the Event entity and then select the timeStamp property in the Properties pane, you will see the details of the timeStamp attribute in the Detail pane. You should see that the timeStamp attribute is optional and that it is a Date type.

Your managed object context will manage the objects defined by the Event entity. Remember that when you created the Persistent Store Coordinator, you initialized it with all of the managed object models in the bundle. Then, when you created the context, it used the Persistent Store Coordinator. Next, you will see how you use the Event entity to create and manage Core Data Managed Objects in code.

RootViewController

The RootViewController is a subclass of UITableViewController and contains the TableView that you will use to display your data. This class has a property that holds the context, which the Application Delegate sets when it creates the RootViewController instance. The RootViewController also has a property that holds an NSFetchedResultsController.

The NSFetchedResultsController class is the glue that binds the results of a fetch request against your datasource to a TableView. You will look at the NSFetchedResultsController class in more detail in Chapter 6.

Figure 5-4 shows a high-level view of how the NSFetchedResultsController class works. The class takes a fetch request and a context as its inputs and calls delegate methods when the data in the fetch request changes. The controller implements methods that you use when implementing the TableView delegate methods that you are familiar with from Chapter 3.

NSFetchedResultsController usage

Figure 5.4. NSFetchedResultsController usage

The first thing to notice is that the RootViewController implements the NSFetchedResultsControllerDelegate protocol. You can see this in the RootViewController.h header file. Remember that declaring that you implement a protocol is a contract that commits you to implementing certain methods. Classes that do not implement this protocol cannot be delegates for an NSFetchedResultsController.

The following is the code for the getter method for the fetchedResultsController property:

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    /*
Set up the fetched results controller.
    */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity =
        [NSEntityDescription entityForName:@"Event"
                    inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor =
        [[NSSortDescriptor alloc] initWithKey:@"timeStamp"
                                    ascending:NO];
    NSArray *sortDescriptors =
        [[NSArray alloc] initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController =
        [[NSFetchedResultsController alloc]
            initWithFetchRequest:fetchRequest
            managedObjectContext:managedObjectContext
            sectionNameKeyPath:nil cacheName:@"Root"];

    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;

    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

    return fetchedResultsController;
}
                                                         
NSFetchedResultsController usage

The first part of the code should be familiar. It checks to see if you have already created the fetchedResultsController. If it already exists, the method returns the controller. If not, the code goes on to create it.

The next section of code creates and configures the objects needed by the fetchedResultsController. As you can see from Figure 5-4, you need a fetch request and a context to be able to use the fetchedResultsController. Because you already have a context, in the managedObjectContext property, the code only needs to create a fetch request.

You can think of a fetch request as a SQL SELECT statement. The code creates a FetchRequest object, creates an entity based on the "Event" entity in the context, and then sets the entity used by the fetchRequest. Next, the code sets the batch size of the fetchRequest to a reasonable number of records to receive at a time.

The next bit of code creates an NSSortDescriptor. You use the NSSortDescriptor to sort the results in the fetchRequest. You can think of the NSSortDescriptor as a SQL ORDER BY clause. Here, you order the result set based on the timeStamp field in descending order. The NSSortDescriptor then sets the sort descriptor used by the fetch request.

Finally, calling the initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: method creates and initializes fetchedResultsController. The template then sets the delegate to self, assigns the fetchedResultsController property, and then the template releases the local objects.

The only delegate method from the NSFetchedResultsControllerDelegate protocol implemented by the template is controllerDidChangeContent:. The fetchedResultsController calls this method when all changes to the objects managed by the controller are complete. In this case, you tell the table to reload its data.

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // In the simplest, most efficient, case, reload the table view.
    [self.tableView reloadData];
}
                                                         
NSFetchedResultsController usage

Now that you have seen how you create and configure the fetchedResultsController, let's look at how you configure the RootViewController at startup. You do this in the viewDidLoad method:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Set up the edit and add buttons.
    self.navigationItem.leftBarButtonItem = self.editButtonItem;

    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
                             target:self
                             action:@selector(insertNewObject)];

    self.navigationItem.rightBarButtonItem = addButton;
    [addButton release];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        //  Log the error
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

        //  Quit
        abort();
    }
}
                                                         
NSFetchedResultsController usage

The method first calls the superclass version of viewDidLoad to ensure that you perform any initialization required by the superclass.

Next, the code configures the Edit button, creates the Add button and adds the buttons to the navigation at the top of the screen. You can see in the initialization of the addButton that the insertNewObject method will be called when someone taps the Add button.

Finally, you call the performFetch: method on the fetchedResultsController to execute the fetch request and retrieve the desired data.

Now, you will look at how you use the TableView delegate methods to display your data. These should be familiar to you from Chapter 3.

The first method is numberOfSectionsInTableView:.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[fetchedResultsController sections] count];
}
                                                         
NSFetchedResultsController usage

As you may recall, the TableView calls this method when it needs to know how many sections to display. Here, the code simply asks the fetchedResultsController for the number of sections.

The next TableView delegate method is numberOfRowsInSection:.

- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {

    id <NSFetchedResultsSectionInfo> sectionInfo =
        [[fetchedResultsController sections] objectAtIndex:section];
    return [sectionInfo numberOfObjects];
}
                                                         
NSFetchedResultsController usage

Again, you call upon the fetchedResultsController to return the number of rows to display.

Finally, you configure the cell in the cellForRowAtIndexPath: method.

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView
                             dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
    }
// Configure the cell.
    NSManagedObject *managedObject = [fetchedResultsController
                                      objectAtIndexPath:indexPath];
    cell.textLabel.text =
        [[managedObject valueForKey:@"timeStamp"] description];

    return cell;
}
                                                         
NSFetchedResultsController usage

This code should be familiar down to the point where it retrieves the managed object. Again, the code asks fetchedResultsController for the object pointed to by the index path. Once it obtains this object, the code uses key-value coding to get the value for the timeStamp property. You learn more about key-value coding in Chapter 6.

The commitEditingStyle:forRowAtIndexPath: method contains the code to handle editing rows in the TableView. The TableView calls this method when editing of the TableView will cause a change to the underlying data. In this case, deleting an object in the TableView should delete the object from the context.

- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
    forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the managed object for the given index path
        NSManagedObjectContext *context =
            [fetchedResultsController managedObjectContext];

        [context deleteObject:
            [fetchedResultsController objectAtIndexPath:indexPath]];

        // Save the context.
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}
                                                         
NSFetchedResultsController usage

The code first determines if the user has deleted a cell. Then, it gets a reference to the context and tells the context to delete the object that was deleted from the TableView. Last, the context changes are committed to disk by calling the save: method.

The last interesting bit of code in the RootViewController is the insertNewObject method. Recall that this is the method that will be called when a user taps the Add button at the top of the screen.

- (void)insertNewObject {

    // Create a new instance of the entity managed by the fetched results
    // controller.
    NSManagedObjectContext *context =
        [fetchedResultsController managedObjectContext];
    NSEntityDescription *entity =
        [[fetchedResultsController fetchRequest] entity];
    NSManagedObject *newManagedObject =
        [NSEntityDescription insertNewObjectForEntityForName:[entity name]
                                      inManagedObjectContext:context];

    // If appropriate, configure the new managed object.
    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];

    // Save the context.
    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
}
                                                         
NSFetchedResultsController usage

Like the commitEditingStyle:forRowAtIndexPath: method, this code first gets a reference to the context. Next, the code creates a new entity based on the entity that the fetchedResultsController uses. The code then creates a new managed object based on that entity and inserts it into the context. Next, the code configures the managed object with the appropriate data; in this case, a timestamp. Finally, the context is committed to disk with the save: method.

Modifying the Template Code

Now that you are familiar with how the template code works, you will modify it to create tasks instead of timestamps. To build the task application, you will modify the data model by creating a Task entity, create a new ViewController that you will use to create tasks, and update the RootViewController to use the new Task entity.

The first thing that you will do is to modify the data model by changing the existing Event entity to make it a Task entity. If you make a change to the data model and attempt to run your application, you will get an error that looks something like this:

2010-03-31 12:50:34.595 Tasks[2424:207] Unresolved error Error
    Domain=NSCocoaErrorDomain Code=134100 UserInfo=0x3d12140
"Operation could not be completed. (Cocoa error 134100.)", {
    metadata =     {
        NSPersistenceFrameworkVersion = 248;
        NSStoreModelVersionHashes =         {
            Location = <fa099c17 c3432901 bbaf6eb3 1dddc734 a9ac14d2 36b913ed
                97ebad53 3e2e5363>;
            Task = <40414517 78c0bd9f 84e09e2a 91478c44 d85394f8 e9bb7e5a
                abb9be27 96761c30>;
        };
NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers =         (
        );
        NSStoreType = SQLite;
        NSStoreUUID = "762ED962-367C-476C-B4BD-076A6D1C33A9";
        "_NSAutoVacuumLevel" = 2;
    };
    reason = "The model used to open the store is incompatible with the one
        used to create the store";
}
                                                         
Modifying the Template Code

This error says that "The model used to open the store is incompatible with the one used to create the store." This means exactly what it sounds like: The data store on the device is not compatible with your revised data model, so Core Data cannot open it. When you encounter this situation, you will need to use Core Data migration to move your data from one data model to another. Core Data migration is covered in detail in Chapter 9. For now, simply delete the existing application from the simulator or device before trying to run it again. This will force Core Data to build a new data store that is compatible with the data model.

Open the Tasks.xcdatamodel file, and then select the Event entity. Change the name of the entity to "Task" in the Detail pane. Add a new property to the Task entity by clicking the plus icon below the Properties pane and selecting Add Attribute. Call the attribute taskText and set its type to String in the Properties pane. You will use the new attribute to store the text for your tasks. Your data model should look like Figure 5-5.

Revised Tasks data model

Figure 5.5. Revised Tasks data model

Next, you need to build a new screen for entering the Task text. In a production application, you would probably want to use Interface Builder to build a nice user interface for this screen. However, to keep this example simple, you will just build a very simple interface in code.

Add a new UIViewController subclass to your project. Make sure you clear the checkboxes for "UITableViewController subclass" and "With XIB for user interface." Call your new class TaskEntryViewController.

Modify the TaskEntryViewController.h header to include instance variables for a UITextField and the Managed Object Context. When the RootViewController calls up the TaskEntryViewController, it will set the reference to the context. Declare the properties for the UITextField and the managed object context. Finally, modify the interface definition to indicate that you will be implementing the UITextFieldDelegate protocol. You use this protocol to receive messages from the UITextField. Specifically, you will be implementing the textFieldShouldReturn: method that runs when the Return button is pressed in a TextField.

The code for the TaskEntryViewController.h header should look like this:

#import <UIKit/UIKit.h>


@interface TaskEntryViewController : UIViewController <UITextFieldDelegate>{
    UITextField *tf;

    NSManagedObjectContext *managedObjectContext;

}

@property (retain, nonatomic) UITextField *tf;
@property (retain, nonatomic) NSManagedObjectContext *managedObjectContext;

@end
                                                         
Revised Tasks data model

Now you will implement the TaskEntryViewController. First, synthesize the TextField and the context properties:

@synthesize tf, managedObjectContext;

Next, you need to add code to TaskEntryViewController.m to programmatically create the UI in the loadView method:

- (void)loadView {
    [super loadView];

    self.tf = [[UITextField alloc] initWithFrame:CGRectMake(65, 20, 200, 20)];
    [self.tf setBackgroundColor:[UIColor lightGrayColor]];
    [self.tf setDelegate:self];
    [self.view addSubview:self.tf];

    UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(5, 20, 60, 20)];
[lbl setText:@"Task:"];

    [self.view addSubview:lbl];
    [lbl release];

}
                                                         
Revised Tasks data model

The code creates a TextField object with the specified screen coordinates. Then, it sets the background color so it is easy to see. The delegate is then set and the code adds the control to the main view. The code also creates a UILabel so that users will know what the text field represents. Remember that when you are creating a production application, you will not need this code. You will most likely use Interface Builder to build a nicer interface than a plain text label and a gray text entry field.

The next step is to add the textFieldShouldReturn: method to TaskEntryViewController.m. The framework calls this UITextFieldDelegate method when Return is pressed. The code inserts a new Task object into the context:

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    // Create a new instance of the entity managed by the fetched results
    // controller.
    NSManagedObjectContext *context = self.managedObjectContext;

    NSEntityDescription *entity = [NSEntityDescription
                                   entityForName:@"Task"
                                   inManagedObjectContext:context];

    NSManagedObject *newManagedObject =
    [NSEntityDescription insertNewObjectForEntityForName:[entity name]
                                  inManagedObjectContext:context];

    // If appropriate, configure the new managed object.
    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
    [newManagedObject setValue:[self.tf text] forKey:@"taskText"];

    // Save the context.
    NSError *error = nil;
    if (![context save:&error]) {
        /*
         Replace this implementation with code to handle the error
         appropriately.

         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    [self dismissModalViewControllerAnimated:YES];
    return YES;
}
                                                         
Revised Tasks data model

First, this code gets a pointer to the context. Then, you define a new Task entity from the context. Next, it inserts a managed object into the context. Then, the code configures the managed object using a timestamp and the text entered in the TextField. The context is then committed to disk and you dismiss the modal view controller. The interface will look like Figure 5-6.

Text entry interface

Figure 5.6. Text entry interface

The last thing to do is modify the RootViewController to use the new Task entity and to call up the Text Entry interface when a user clicks the plus button to enter a new task.

Because you will be referencing the TaskEntryViewController, you will need to add an import statement to RootViewController.m for TaskEntryViewController.h:

#import "TaskEntryViewController.h"

Next, you need to modify the fetchedResultsController getter method to use the Task entity instead of the Event entity. I have presented the modified code in bold.

- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {
        return fetchedResultsController;
    }

    /*
     Set up the fetched results controller.
    */
    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Task"
        inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];

...
                                                         
Text entry interface

Now, delete the old insertNewObject method. You will need to recode this method to present the TaskEntryViewController like this:

- (void)insertNewObject {

    //  Ask for text from TaskEntryViewController
    TaskEntryViewController *tevc = [[TaskEntryViewController alloc] init];
tevc.managedObjectContext = self.managedObjectContext;

    [self presentModalViewController:tevc
                            animated:YES];

     [tevc release];


}
                                                         
Text entry interface

This code simply creates a TaskEntryViewController, sets the context, and presents it as a modal controller.

The final step is to modify the tableView:(UITableView *)tableView cellForRowAtIndexPath: method to use the taskText attribute for the main text and the timeStamp attribute for the detail text:

- (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];
    }

    // Configure the cell.
    NSManagedObject *managedObject = [fetchedResultsController
                                      objectAtIndexPath:indexPath];

    cell.textLabel.text =
        [[managedObject valueForKey:@"taskText"] description];
    cell.detailTextLabel.text =
        [[managedObject valueForKey:@"timeStamp"] description];
    return cell;
}
                                                         
Text entry interface

First, this method tries to dequeue a cell. If it fails, it creates a new cell using the UITableViewCellStyleSubtitle cell style so that you can display both the task text and the timestamp. Next, the code retrieves that managed object for the cell using the fetchedResultsController. Then, the code sets the cell's textLabel.text property to the string contained in the taskText attribute of the managed object and sets the detailTextLabel.text property to the timeStamp attribute.

You should now be able to build and run the application. Try clicking on the plus icon to add new tasks. After typing in the text and hitting Return, you should see your task listed in the TableView. Try adding a few more tasks and then swiping across a task in the TableView. You should see a Delete button, which will allow you to delete the task. Tap the Edit button and notice that you go into edit mode. If you tap a red circle icon in Edit mode, you see a Delete button, which allows you to delete the row. The completed application should look like Figure 5-2.

MOVING FORWARD

In this chapter, you have learned about the basics of Core Data. You examined the Core Data architecture and you learned the basic terminology necessary to understand the Core Data API stack. Then, you explored the template code to understand how to implement the Core Data architecture. Finally, you modified the template application to produce a functional task manager application.

In the next few chapters, your knowledge of Core Data will go from cursory to detailed. In the next chapter, you learn to use the Xcode data modeler to develop a model that incorporates many of the features of Core Data. Then, in Chapter 7 you learn how to bring that model to life through code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset