Using Core Data: A Simple Task Manager ❘ 129 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.m The code fi rst 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 confi guring 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. CH005.indd 129CH005.indd 129 9/18/10 9:34:23 AM9/18/10 9:34:23 AM 130 ❘ CHAPTER 5 INTRODUCING CORE DATA 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.m 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 fi les contained in the application bundle. The Tasks.xcdatamodel fi le in the Resources folder contains the object model. This fi le is included with the application bundle, so this method takes that fi le 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.m This method works similarly to the previous two. The fi rst 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. CH005.indd 130CH005.indd 130 9/18/10 9:34:23 AM9/18/10 9:34:23 AM Using Core Data: A Simple Task Manager ❘ 131 I hope that the way in which all of the objects in the Core Data stack fi t 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. 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. CH005.indd 131CH005.indd 131 9/18/10 9:34:24 AM9/18/10 9:34:24 AM 132 ❘ CHAPTER 5 INTRODUCING CORE DATA 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 defi ned 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. The fi rst thing to notice is that the RootViewController implements the NSFetchedResultsControllerDelegate protocol. You can see this in the RootViewController.h header fi le. 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; } /* NSFetchedResultsController Fetched Results Controller Delegate Fetch Request Managed Object Context FIGURE 5 - 4: NSFetchedResultsController usage CH005.indd 132CH005.indd 132 9/18/10 9:34:24 AM9/18/10 9:34:24 AM Using Core Data: A Simple Task Manager ❘ 133 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; } RootViewController.m The fi rst 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 confi gures 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 CH005.indd 133CH005.indd 133 9/18/10 9:34:25 AM9/18/10 9:34:25 AM 134 ❘ CHAPTER 5 INTRODUCING CORE DATA 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 fi eld 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]; } RootViewController.m Now that you have seen how you create and confi gure the fetchedResultsController , let ’ s look at how you confi gure 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(); } } RootViewController.m CH005.indd 134CH005.indd 134 9/18/10 9:34:25 AM9/18/10 9:34:25 AM Using Core Data: A Simple Task Manager ❘ 135 The method fi rst calls the superclass version of viewDidLoad to ensure that you perform any initialization required by the superclass. Next, the code confi gures 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 fi rst method is numberOfSectionsInTableView :. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[fetchedResultsController sections] count]; } RootViewController.m 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]; } RootViewController.m Again, you call upon the fetchedResultsController to return the number of rows to display. Finally, you confi gure 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]; } CH005.indd 135CH005.indd 135 9/18/10 9:34:26 AM9/18/10 9:34:26 AM 136 ❘ CHAPTER 5 INTRODUCING CORE DATA // Configure the cell. NSManagedObject *managedObject = [fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = [[managedObject valueForKey:@”timeStamp”] description]; return cell; } RootViewController.m 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(); } } } RootViewController.m The code fi rst 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. CH005.indd 136CH005.indd 136 9/18/10 9:34:27 AM9/18/10 9:34:27 AM Using Core Data: A Simple Task Manager ❘ 137 - (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(); } } RootViewController.m Like the commitEditingStyle:forRowAtIndexPath: method, this code fi rst 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 confi gures 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 fi rst 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 > ; }; CH005.indd 137CH005.indd 137 9/18/10 9:34:27 AM9/18/10 9:34:27 AM 138 ❘ CHAPTER 5 INTRODUCING CORE DATA 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”; } RootViewController.m 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 fi le, 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. FIGURE 5 - 5: Revised Tasks data model CH005.indd 138CH005.indd 138 9/18/10 9:34:28 AM9/18/10 9:34:28 AM . 3; NSStoreModelVersionIdentifiers = ( ); NSStoreType = SQLite; NSStoreUUID = “762ED96 2-3 67C-476C-B4BD-076A6D1C33A9”; “_NSAutoVacuumLevel” = 2; }; reason = “The model used to open the store. you open the Resources folder in Xcode and double - click on Tasks.xcdatamodel , you should see something similar to Figure 5 - 3. FIGURE 5 - 3: The default Object Model The code template. detail on using the data - modeling tool. CH005.indd 131CH005.indd 131 9/18/10 9:34:24 AM9/18/10 9:34:24 AM 132 ❘ CHAPTER 5 INTRODUCING CORE DATA You can see in Figure 5 - 3 that the Event entity