Chapter 5. Putting Core Data to Work

In This Chapter

  • Using Core Data

  • Figuring out fetch requests

  • Adding, saving, and loading Hotel objects with Core Data

  • Supporting Undo

When you have the infrastructure in place, as spelled out in Chapter 4 of this minibook, you still need to do one more thing to get Core Data up and running: You need to actually create the Managed Object Context in your application.

After you do that, you need to send a message to the Managed Object Context to create and delete objects as well as support iPhone's Shake feature to do undos and redos. You'll also create fetch requests to retrieve data using the Managed Object Context.

I show you how to do all of that in this chapter. But before you start, I want to explain the last major concept you need to understand to use Core Data: the fetch request.

Understanding the Fetch Request

A fetch request is an object that specifies what data you want, for example, "all Hotels ordered by city," or "all Hotels in Needles, California."

A fetch request has three parts:

  1. The name of an entity. (You can only fetch one type of entity at a time.)

  2. A predicate object that specifies conditions that objects must match.

  3. An array of sort descriptor objects that specifies the order in which the objects should appear.

You send a fetch request to a Managed Object Context, which gets the objects that match your request (possibly none), adds them to the Managed Object Context, and returns them to the application. If a context already contains a managed object for an object returned from a fetch, then the existing managed object is returned in the fetch results.

The framework tries to be as efficient as possible — you get only the objects you asked for. If you follow a relationship to an object that hasn't yet been fetched, it's fetched automatically for you. If you stop using an object, by default it will be deallocated. (This is, of course, not the same as deleting it.)

Unless you really need all the objects of a particular entity, you can also use a predicate to limit the number of objects returned to those you're actually interested in.

In addition, your fetch request can also cache the results so that if the same data is subsequently needed, the work doesn't have to be repeated. (I explain caching later as well.)

If you're displaying objects in a table view, you can use a fetched results controller, NSFetchedResultsController. It, too, tries to minimize the amount of data held in memory. It efficiently manages the results returned from a Core Data fetch request to provide data for a UITableView object. You use this class to get the objects you need to create cells in such an object, as well as to determine the number of sections you need and the number of rows in each section.

Finally, as you'll soon see, a fetch request controller can monitor changes to objects and report changes to a delegate, which can then do what's necessary to update the table view accordingly (including reloading the data).

By the way, you just happen to create and use a fetched results controller later in this chapter.

Getting the Ball Rolling

To get everything up and going, you need to actually instantiate (or create) the Core Data Stack you added in the last chapter. You do that in the Trip initialization method.

Modify initWithName in Trip.m by using the bolded code in Listing 5-1.

Example 5-1. Modifying initWithName and Synthesizing Hotels

@synthesize hotels;

- (id) initWithName:(NSString*) theTrip {

  if ((self = [super init])) {

    [self managedObjectContext];
    tripName = theTrip;
    [tripName retain];
    carInformation = [[CarInformation alloc]
   initWithTrip:self];
    carServicing = [[CarServicing alloc] initWithTrip:self];
    weather = [[Weather alloc] init];
    [self loadSights];
    [self loadHotels];
    map =[[Map alloc] initWithTrip:self];
  }
  return self;
}

Walking through the code, you see that you create the Core Data Stack by simply making a reference to the managedObjectContext.

[self managedObjectContext];

As explained in the last chapter, that statement invokes the managedObjectContext property's getter method which you have overridden. In that method, you create an NSManagedObjectContext (if it doesn't already exist), which in turn references the persistentStoreCoordinator, whose property getter method you also override. In this method, you create the NSPersistentStoreCoordinator (if it doesn't exist) and reference the managedObjectModel property. You override that method, and managedObjectModel getter creates the NSManagedObjectModel if it doesn't exist.

Further down in Listing 5-1, you'll notice that you synthesize a property called hotels — you'll use this to hold an array of Hotel objects.

Finally, in the initWithName method in Trip.m, you send a message to yourself to loadHotels.

Not bad for a start. Now, enter the code in Listing 5-2 to implement load-Hotels.

Example 5-2. loadHotels

-(void) loadHotels{
  NSFetchRequest *fetchRequest =
                            [[NSFetchRequest alloc] init];
  // Edit the entity name as appropriate.
  NSEntityDescription *entity = [NSEntityDescription
   entityForName:@"Hotel" inManagedObjectContext:managedObject
   Context];
  [fetchRequest setEntity:entity];

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

  // Create the sort descriptors array.
  NSSortDescriptor *sortDescriptor =
                 [[NSSortDescriptor alloc]
                 initWithKey:@"city" ascending:YES];
  NSArray *sortDescriptors =
    [[NSArray alloc] initWithObjects:sortDescriptor, nil];
  [fetchRequest setSortDescriptors:sortDescriptors];

 // Execute the fetch.
  NSError *error = nil;
  NSMutableArray *mutableFetchResults =
   [[managedObjectContext executeFetchRequest:fetchRequest
                               error:&error] mutableCopy];
  if (mutableFetchResults == nil) {
     // Handle the error.
  }
  [self setHotels:mutableFetchResults];

  [fetchRequest release];
  [sortDescriptor release];
  [sortDescriptors release];
}

I've based this code on the templates provided in Xcode. Now I can take you through it step by step.

First you start by creating and initializing an NSFetchRequest.

NSFetchRequest *fetchRequest =
                            [[NSFetchRequest alloc] init];

You then create an NSEntityDescription object with the name of the entity (Hotel) and the Managed Object Context and bind the object to the fetch request.

NSEntityDescription *entity = [NSEntityDescription
   entityForName:@"Hotel" inManagedObjectContext:managedObject
   Context];
[fetchRequest setEntity:entity];

This takes care of the first part of a fetch request.

Next, you set the estimated batch size. Here, I tell the Managed Object Context that the most I want "fetched" is the first 20 Hotel objects.

[fetchRequest setFetchBatchSize:20];

You can use this feature to control the memory footprint of your application. The default value of 0 is treated as infinite. But, if you set a non-zero batch size, the collection of objects returned when the fetch is executed is broken into batches — and no more than batchSize objects' data will be fetched from the persistent store at a time. If you need an object that isn't in the batch, Core Data will go out and get it for you. (Okay, part 2 done.)

Then, you create the sort descriptor arrays and specify that you want the returned fetch to be sorted by city (initWithKey:@"city") in ascending order (ascending:YES), add then the sort descriptor and bind it to the fetch request. This task completes the third part of the fetch request.

NSSortDescriptor *sortDescriptor =
        [[NSSortDescriptor alloc] initWithKey:@"city"
                                          ascending:YES];
NSArray *sortDescriptors =
    [[NSArray alloc] initWithObjects:sortDescriptor, nil];
fetchRequest setSortDescriptors:sortDescriptors];

You won't be working with a predicates here, so you finish up by executing the fetch.

NSError *error = nil;
NSMutableArray *mutableFetchResults =
   [[managedObjectContext executeFetchRequest:fetchRequest
                               error:&error] mutableCopy];
if (mutableFetchResults == nil) {
  // Handle the error.
}

Finally, you set the hotels property to the mutable fetch results you just created.

[self setHotels:mutableFetchResults];

And then clean up after yourself.

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

To finish up with all the changes you need to make to Trip, add the code in Listing 5-3 to Trip.h. You're adding the hotels property that you used to hold all your various Hotel objects and then declaring the loadHotels method you just coded.

Example 5-3. Modifying Trip.h

#import <MapKit/MapKit.h>
#import <Foundation/Foundation.h>
@class Weather;
@class CarServicing;
@class CarInformation;
@class Map;
@class MapController;

@interface Trip : NSObject  {

  NSString                     *tripName;
  CarServicing                 *carServicing;
  CarInformation               *carInformation;
  Weather                      *weather;
  NSMutableArray               *sights;
  Map                          *map;
  MapController                *mapController;
  NSMutableArray               *hotels;



  // Core Data Stack
  NSManagedObjectModel         *managedObjectModel;
  NSManagedObjectContext       *managedObjectContext;
  NSPersistentStoreCoordinator *persistentStoreCoordinator;

}

@property (nonatomic, retain) NSMutableArray *sights;
@property (nonatomic, retain) Map* map;
@property (nonatomic, retain) MapController *mapController;
 @property (nonatomic, retain) NSMutableArray * hotels;


- (id) initWithName:(NSString*) theTripName;
- (CLLocationCoordinate2D) initialCoordinate;
- (NSString*) mapTitle;
- (id) returnSightDelegate:(int) index;
- (id) returnWeatherDelegate;
- (id) returnCarInformationDelegate;
- (id) returnCarServicingDelegate;
- (void) loadHotels;
// Core data properties and method
@property (nonatomic, retain, readonly) NSManagedObjectModel
    *managedObjectModel;
@property (nonatomic, retain, readonly)
    NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly)
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (NSString *)applicationDocumentsDirectory;

@end

In Chapter 3 of this minibook, you did a lot of work to make adding annotations to the map easy. Now your work will start to pay off. All you need to do to have the Hotel object's annotations appear on the map when you fetch them from the store is make the modifications bolded in Listing 5-4 to createAnnotations in Map.m.

Example 5-4. Loading the Annotation Objects

- (void) createAnnotations {

  [annotations removeAllObjects];
  [annotations addObjectsFromArray:trip.sights ];
  for (Hotel* aHotel in trip.hotels) {
    [annotations addObject:aHotel.annotation ];
  }
}

This code gets the Annotation object referenced by a Hotel and adds it to the annotations array that the Map view will then use to display the annotations.

You also need to add the following to Map.m.

#import "Hotel.h"

Finally, you need to enable the selection of the HotelController in the RootViewController. I know, I know, I haven't even coded the HotelController yet, but I want to get the mundane out of the way. Add the code in Listing 5-5 to tableView:didSelectRowAtIndexPath in RootViewController.m that uncomments out the selection of the HotelController.

Example 5-5. Uncommenting Out HotelController Selection

- (void)tableView:(UITableView *)tableView
        didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

  RoadTripAppDelegate *appDelegate =
              (RoadTripAppDelegate *)
             [[UIApplication sharedApplication] delegate];
  [appDelegate.lastView replaceObjectAtIndex:0 withObject:
          [NSNumber numberWithInteger:indexPath.section]];
  [appDelegate.lastView replaceObjectAtIndex:1 withObject:
          [NSNumber numberWithInteger:indexPath.row]];

  [tableView deselectRowAtIndexPath:indexPath
                                            animated:YES];

  int menuOffset =
             [self menuOffsetForRowAtIndexPath:indexPath];
  UIViewController *targetController =
             [[menuList objectAtIndex:menuOffset]
                             objectForKey:kControllerKey];

  if (targetController == nil) {
    BOOL realtime = !appDelegate.useStoredData;

    switch (menuOffset) {
      case 0:
        if (realtime) targetController =
                [[MapController alloc] initWithTrip:trip];
        else [self displayOfflineAlert:
                [[menuList objectAtIndex:menuOffset]
                                objectForKey:kSelectKey]];
        break;
      case 1:
        targetController =
          [[SightListController alloc] initWithTrip:trip];
        break;
      case 2:
        targetController =
              [[HotelController alloc] initWithTrip:trip];
        break;
      case 3:
        if  (realtime) targetController = [[WebViewController
   alloc] initWithTrip:trip tripSelector:@
   selector(returnWeather:)
           webControl:YES title:NSLocalizedString(@"Weather",
                                             @"Weather")];
        else [self displayOfflineAlert:
             [[menuList objectAtIndex:menuOffset]
                                objectForKey:kSelectKey]];
        break;
      case 4:
targetController = [[WebViewController alloc]
   initWithTrip:trip tripSelector:@selector(returnCarServicing
   Information:)
                            webControl:NO
   title:NSLocalizedString (@"Car Servicing", @"Car
   Servicing")];
        break;
      case 5:
        targetController = [[WebViewController alloc]
           initWithTrip:trip
           tripSelector:@selector(returnCarInformation:)
           webControl:NO
           title:NSLocalizedString (@"The Car", @"Car
                                           Information")];
        break;
    }
        if (targetController) {
      [[menuList  objectAtIndex:menuOffset]
   setObject:targetController forKey:kControllerKey];
    }
  }
  if (targetController) {
    [[self navigationController]
        pushViewController:targetController animated:YES];
    [targetController release];
  }
}

You also have to add the import to RootViewController.m.

#import "HotelController.h"

Adding HotelController

All that lovely coding in the preceding section got you to the point where you can select an entry in the table view that creates a HotelController. It's time to add the code that creates a HotelController when you make that selection.

This is the last piece of the puzzle you need in order to create and display Hotel objects.

As I explain in Chapter 4 of this minibook, if you had selected that Use Core Data for Storage check box in your Navigation-based Application template right at the beginning of your project, your RootViewController would have been generated with code to use Core Data. So, in the same spirit of copy and paste that you used in Chapter 4 to add the Core Data Stack, you'll add the code generated by the template to your HotelController.

But first you need to create the controller:

  1. In the RoadTrip project window, select the Classes folder and then choose File

    Adding HotelController
  2. In the left column of the dialog, select Cocoa Touch Classes under the iPhone OS heading, select the UIViewController subclass template in the top-right pane, and then click Next.

    Be sure the UITableViewController subclass is selected and the With XIB for User Interface is also selected.

    You see a new dialog asking for some more information.

  3. Enter HotelController.m in the File Name field and then click Finish.

Tip

To make things easier to find, I keep my HotelController.m and .h classes in the Classes folder. I also move the HotelController.xib to the Resources folder

Setting up the controller

Start with the interface file. Modify HotelController.h with the bolded code in Listing 5-6.

Example 5-6. HotelController.h

@class Trip;
@class Hotel;

@interface HotelController : UITableViewController
                    <NSFetchedResultsControllerDelegate> {
  NSFetchedResultsController *fetchedResultsController;
  Trip                       *trip;
  Hotel                      *hotel;
}
@property (nonatomic, retain) NSFetchedResultsController
                               *fetchedResultsController;
- (id) initWithTrip: (Trip*) aTrip;
@end

First of all, you have made HotelController an NSFetchedResultsControllerDelegate here. You'll see why you did that in the "Creating a fetched results controller" section, later in this chapter.

You've also declared three instance variables. One of them holds a reference to Trip, the second will hold a reference to a new Hotel object when you do finally create one, and the third is the instance variable behind the fetchedResultsController property that you also add.

Finally, you declare the initWithTrip: method you're going to add to HotelController.m.

To implement initWithTrip:, add the bolded code in Listing 5-7 to HotelController.m.

Example 5-7. initWithTrip

#import "HotelController.h"
#import "MapController.h"
#import "Trip.h"
#import "Hotel.h"
#import "Annotation.h"
#import "Geocoder.h"

@implementation HotelController

@synthesize fetchedResultsController;

- (id) initWithTrip: (Trip*) aTrip{
  if (self = [super initWithNibName:@"HotelController"
                                            bundle:nil]) {
    trip = aTrip;
    [trip retain];
  }
  return self;
}

You'll notice I had you add a number of #import statements. You don't need them now, but you will need them later, so why not get that out of the way?

Repurposing the code from the template

Working in the same spirit of copy and paste that you used in Chapter 4 of this minibook to add the Core Data Stack, you'll add the template code to HotelController. Here's how:

  1. Open the CoreData project you created in the last chapter.

  2. Select all the code in the RootViewController, starting with viewDidLoad.

  3. Delete all the code after initWithTrip: in HotelController.m and replace it with the code you copied from RootViewController in the CoreData project.

Admittedly, your work isn't done yet, but this copy and paste stuff has gotten you pretty far, even if you still need to make some (minor) modifications to some of the code. You'll start with viewDidLoad.

Note

You may notice some stylistic differences in the code I'm about to have you work with. That's because, as much as possible, I've retained the code that would have been generated by using a Core Data template when you first set up your project.

Modifying viewDidLoad

Make the modifications indicated in bold in Listing 5-8 to the copy of viewDidLoad in HotelController.m.

Example 5-8. Modifying viewDidLoad

- (void)viewDidLoad {
  [super viewDidLoad];

// Set up the edit and add buttons.
//self.navigationItem.leftBarButtonItem =
                                 self.editButtonItem;
self.title = @"Hotels";

UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
   initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
 target:self action:@selector(insertNewObject)];
self.navigationItem.rightBarButtonItem = addButton;
[addButton release];
self.tableView.sectionHeaderHeight = 40.0;


NSError *error = nil;
if (![[self fetchedResultsController]
                                 performFetch:&error]) {

  //Replace this implementation with code to handle the
  // error appropriately.
  SLog(@"Unresolved error %@, %@", error,
                                       error userInfo]);
  abort(); //$$ not in production
  }
}

Looking at Listing 5-8, you see that the template code you got from your CoreData project adds an Edit button, which you won't be using, so I have you delete that. It also adds an Add button, which the user taps to add a new hotel. This Add button has the insertNewObject method as its target.

I also have you add a line of code to set the title of the Navigation bar to "Hotels."

The default table view is plain, and that's what you'll be using. You can, however, also have section titles, but I find the default Height value a little cramped, so I have you set it here.

self.tableView.sectionHeaderHeight = 40.0;

Creating a fetched results controller

As I mention in the opening of this chapter, if you're using a table view, a fetched results controller (NSFetchedResultsController) can make your life much easier. It, too, tries to minimize the amount of data held in memory by efficiently managing the results returned from a Core Data fetch request to provide data for a UITableView object. You use this class to get the objects you need to create cells in a UITableView, as well as to determine both the number of sections you need and the number of rows for each section.

You create an instance of this class by using a fetch request that specifies the entity, an array containing at least one sort ordering, and optionally a filter predicate. You can also specify a sectionNameKeyPath argument that the controller will use to split the results into sections, or you can pass nil to indicate that the controller should generate a single section. After creating an instance, you invoke performFetch: to actually execute the fetch.

To get all this to work for you, make the modifications in bold in Listing 5-9 to fetchedResultsController method in HotelController.m.

Example 5-9. fetchedResultsController

- (NSFetchedResultsController *)fetchedResultsController {

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

// Create the fetch request for the entity.
  NSFetchRequest *fetchRequest =
                            [[NSFetchRequest alloc] init];
   // Edit the entity name as appropriate.
// NSEntityDescription *entity = [NSEntityDescription
   entityForName:@"Event" inManagedObjectContext:managed
   ObjectContext];
  NSEntityDescription *entity = [NSEntityDescription
        entityForName:@"Hotel"
       inManagedObjectContext:trip.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
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor
     alloc] initWithKey:@"city" ascending:NO];
    NSArray *sortDescriptors = [[NSArray alloc]
     initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];

   // Edit the section name key path and cache name
  // nil for section name key path means "no sections".
  //NSFetchedResultsController *aFetchedResultsController =
     [[NSFetchedResultsController alloc] initWithFetchRequest
     :fetchRequest managedObjectContext:managedObjectContext
     sectionNameKeyPath:nil cacheName:@"Root"];
    NSFetchedResultsController *aFetchedResultsController =
     [[NSFetchedResultsController alloc] initWithFetchRequest:
     fetchRequest managedObjectContext:trip.managedObjectContext
     sectionNameKeyPath:@"city" cacheName:@"Root"];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController =
                                aFetchedResultsController;
    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

    return fetchedResultsController;
  }

The first thing you do to set up your very own fetched results controller is create the fetch request for the entity. For all practical purposes, this is the same thing you did in loadHotels, so if you aren't clear on what you've just accomplished in that regard with Listing 5-9, refer to the discussion following Listing 5-3.

What's new here is that, after you've created the fetch request, you create the fetched results controller, specify the section name key path and cache name, and then set self as the delegate.

NSFetchedResultsController *aFetchedResultsController =
   [[NSFetchedResultsController alloc] initWithFetchRequest:f
   etchRequest managedObjectContext:trip.managedObjectContext
   sectionNameKeyPath:@"city" cacheName:@"Root"];
aFetchedResultsController.delegate = self;

With this lovely bit of coding, you create an NSFetchedResults Controller, initializing it with the fetchRequest you just created and configured, the managedObjectContext and two other arguments — sectionNameKeyPath and cacheName.

By passing in the sectionNameKeyPath argument @"city", you're telling Core Data you want to assign Hotel objects to sections in the table view based on the City key. That means that when you're done, your HotelController will display hotels by cities, as shown in Figure 5-1.

The hotel is listed under its city.

Figure 5-1. The hotel is listed under its city.

Giving a (optional) cacheName of @"Root" and a delegate — aFetched ResultsController.delegate = self;, (earlier you had the HotelController adopt the NSFetchedResultsControllerDelegate protocol) tells Core Data to cache its results so that if the same data is subsequently asked for again, the work doesn't have to be repeated.

Finally, you clean up by releasing the objects:

self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];

return fetchedResultsController;

Adding a new hotel

Now that you have the fetched results controller set up, it will be easier to understand what you do when you actually add a hotel.

The most exciting method in the HotelController is the one that adds a new object. The insertNewObject method is the target of the Add button you added in Listing 5-8. Although I've added to this method, it is essentially the same one used in the template. In the template, you simply add a new Event object with a single attribute — a time stamp — which you initialize it with. You will be extending that to create the Hotel object and its Annotation and then add it to the managed object context.

To avoid making things more complicated than they need to be, you won't be developing the user interface in this chapter to do that. The Hotel object and its Annotation are hard coded — that will make it easier for you to follow the logic.

In Book VII, however, I'll have you add a user interface as well as show you how you'll be able to use existing contacts in your Address Book to add hotels or, alternatively, add your new hotel to your Address Book contacts.

I've indicated in bold the changes you'll need to make to the template code to add a new Hotel object and its Annotation object. I've left out or summarized most of the comments you'll see in the template code.

Make the modifications in Listing 5-10 to insertNewObject in HotelController.m.

Example 5-10. Adding a New Hotel

#pragma mark -
#pragma mark Add a new object

- (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 insertNewObjectForEntityFor
   Name:[entity name] inManagedObjectContext:context];
  hotel = [NSEntityDescription
       insertNewObjectForEntityForName:[entity name]
       inManagedObjectContext:trip.managedObjectContext];
// If appropriate, configure the new managed object.
// [newManagedObject setValue:[NSDate date]
   forKey:@"timeStamp"];
  [hotel setValue:@"Hotel California" forKey:@"name"];
  [hotel setValue:@"1 Dr. Carlton B. Goodlett Place"
                                           forKey:@"street"];
  [hotel setValue:@"San Francisco" forKey:@"city"];
  [hotel setValue:@"California" forKey:@"state"];
  [hotel setValue:@"94102" forKey:@"zip"];

  Annotation *annotation = [NSEntityDescription
       insertNewObjectForEntityForName:@"Annotation"
       inManagedObjectContext:trip.managedObjectContext];
  [annotation setTitle:@"Annotation"];
  [annotation setHotel:hotel];
  [hotel setAnnotation:annotation];

  Geocoder * geocoder = [[Geocoder alloc] init];
  NSString* geocodeString = [[NSString alloc ]
        initWithFormat: @" %@ %@ %@ %@",
        hotel.street, hotel.city, hotel.state, hotel.zip];
  CLLocationCoordinate2D theCoordinate =
                 [geocoder geocodeLocation:geocodeString];

  hotel.annotation.latitude =
      [NSNumber numberWithDouble: theCoordinate.latitude];
  hotel.annotation.longitude =
      [NSNumber numberWithDouble: theCoordinate.longitude];
  hotel.annotation.title = hotel.name;
// Save the context.
  NSError *error = nil;
  if (![trip.managedObjectContext save:&error]) {
   // Replace this implementation with code to handle the
   // error appropriately.
  NSLog(@"Unresolved error %@, %@", error,
                                        [error userInfo]);
  abort();
  }
  [trip loadHotels];
  [trip.mapController refreshAnnotations];
}

Looking at Listing 5-10, you see that you start by creating a new instance of the Hotel entity managed by the fetched results controller. (Remember, when you set up the fetch results controller in the previous section you specified the entity you wanted was Hotel.)

NSEntityDescription *entity =
      [[fetchedResultsController fetchRequest] entity];

Then you create a new object using the entity name Hotel in the managed object context you created in Trip.

hotel = [NSEntityDescription
       insertNewObjectForEntityForName:[entity name]
       inManagedObjectContext:trip.managedObjectContext];

Then you set the attributes for the object. (Later you will be doing this in a dialog where the user can enter the information.)

[hotel setValue:@"Hotel California" forKey:@"name"];
[hotel setValue:@"1 Dr. Carlton B. Goodlett Place"
                                       forKey:@"street"];
[hotel setValue:@"San Francisco" forKey:@"city"];
[hotel setValue:@"California" forKey:@"state"];
[hotel setValue:@"94102" forKey:@"zip"];

Next, you need to create and configure the Annotation.

Note

Creating an object doesn't automatically create the objects it has relationships to.

Annotation *annotation = [NSEntityDescription
       insertNewObjectForEntityForName:@"Annotation"
       inManagedObjectContext:trip.managedObjectContext];
[annotation setTitle:@"Annotation"];
[annotation setHotel:hotel];
[hotel setAnnotation:annotation];

Then you use your old friend the geocoder to get the coordinates needed for the Annotation as well as set them.

Geocoder * geocoder = [[Geocoder alloc] init];
NSString* geocodeString = [[NSString alloc ]
        initWithFormat: @" %@ %@ %@ %@",
        hotel.street, hotel.city, hotel.state, hotel.zip];
CLLocationCoordinate2D theCoordinate =
                 [geocoder geocodeLocation:geocodeString];
hotel.annotation.latitude =
      [NSNumber numberWithDouble: theCoordinate.latitude];
hotel.annotation.longitude =
      [NSNumber numberWithDouble: theCoordinate.longitude];
hotel.annotation.title = hotel.name;

Finally, you save the context

NSError *error = nil;
if (![trip.managedObjectContext save:&error]) {
 // Replace this implementation with code to handle the
 // error appropriately.
NSLog(@"Unresolved error %@, %@", error,
                                      [error userInfo]);
abort();
}

and then load the new Hotel into the hotels array used by Map's createAnnotation method and then send a message to the MapController to refresh the annotations.

[trip loadHotels];
[trip.mapController refreshAnnotations];

Here's where your previous work really pays off — you already have the logic in place to dynamically add and delete annotations as needed.

Table view methods

At this point you have created the infrastructure necessary to add a hotel. Of course, this superb piece of engineering isn't worth much until you add the code to the RoadTrip app that actually displays the hotel you have added.

That's what you will do in this section.

Next you need to make some changes to the default table view methods. You'll start with tableView:cellForRowAtIndexPath:. Make the modifications to HotelController.m shown in bold in Listing 5-11.

Example 5-11. tableView:cellForRowAtIndexPath

- (UITableViewCell *)tableView:(UITableView *)tableView cellF
   orRowAtIndexPath:(NSIndexPath *)indexPath {

  static NSString *CellIdentifier = @"Cell";

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

    return cell;
  }

Fetched results controllers are very helpful in table views because they make it easy to get the right number of sections and rows, as well as the information needed to create the cells. Here, as you can see, all you have to do is pass the fetched results controller the index path you get in this method and then it will return the object that has the data (the name of the hotel) you want to display in the cell.

NSManagedObject *managedObject =
 [fetchedResultsController objectAtIndexPath:indexPath];

You set the text in the cell to the name of the hotel.

cell.textLabel.text =
       [[managedObject valueForKey:@"name"] description];

The template code doesn't come with section heads, so you need to add the method in Listing 5-12 to HotelController.m to take care of the section heads we need.

Example 5-12. tableView:titleForHeaderInSection:

- (NSString *)tableView:(UITableView *)tableView titleForHead
   erInSection:(NSInteger)section {
  return [[[fetchedResultsController sections]
   objectAtIndex:section] name];
}

Bunches of other table view methods are available out there, but I'll leave you to explore them on your own. I do, however, come back and revisit table View:didSelectRowAtIndexPath: in the "Adding Delete" section, later in the chapter.

The fetched results controller's delegate

If you set a delegate for a fetched results controller, the controller registers to receive change notifications from its managed object context. (I glossed over that when I discussed caching earlier.) Any change in the managed object context to objects or section information needs to be processed and the display in the table view updated accordingly. The controller notifies the delegate when result objects change location or when sections are modified.

Listing 5-13 shows the simplest case — yes, just reloading the table view counts as change.

Example 5-13. Reloading the Table View

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

This is what you'll be using. There are also a number of other methods available that are beyond the scope of this book. You can read the comments in the generated code and see how they're implemented in the Core Data samples Apple provides.

Note

If you don't set a delegate, the controller does not monitor changes to objects in its associated managed object context — which may be exactly what you want if there aren't going to be any changes (a read-only list, for example).

Managing memory

Finally, because you don't create the managed object context (it is created in Trip), you don't want to delete it either. Make the modifications shown in bold in Listing 5-14 to dealloc in HotelController.m to make sure your managed object context doesn't get tossed.

Example 5-14. dealloc

- (void)dealloc {
   [fetchedResultsController release];
//[managedObjectContext release];
    [super dealloc];
}
@end

You now have someplace to stay

At this point, you can compile and run the code. If you select Hotels in the main menu of your RoadTrip app, you'll be faced with the blank screen you see in Figure 5-2.

The initial Hotels list.

Figure 5-2. The initial Hotels list.

If you select the Add button (+), you'll see in Figure 5-3 that you have now successfully added a hotel.

Even cooler than that, if you go back to the Map view, you'll see (in Figure 5-4) that the hotel is now nicely displayed — with it annotation no less!

Supporting Undo

One of the great things about Core Data is that it makes it easy to support the iPhone's Shake to Undo functionality.

Now, managed objects are always associated with a Managed Object Context, which doesn't sound too surprising. But did you know that each managed object context has an Undo manager? Now that's cool. You can, for example, change a managed object's properties using an accessor method (or key-value coding, or custom key-value-observing compliant methods) and the Managed Object Context uses key-value observing to keep track of modifications to its registered objects. The place where the managed object context registers all its modification events is with its Undo manager.

I wonder what checkout is like?

Figure 5-3. I wonder what checkout is like?

You can find your hotel on the map when you have to stop for the night.

Figure 5-4. You can find your hotel on the map when you have to stop for the night.

To undo an operation, you simply send the context an Undo message; to redo an operation, you send the context — you guessed it — a Redo message. (You can also roll back all changes made since the last save operation by using rollback, as well as other standard Undo manager functionality, such grouping undo events. — something I won't cover here).

Supporting undo in HotelController

To begin all the undoing, the strategy here is to set up the Undo manager when you add a hotel. Make the changes marked in bold/strikeout in Listing 5-15 to the insertNewObject method in HotelController.h.

Example 5-15. Adding the Code to a New Object

#pragma mark -
#pragma mark Add a new object

- (void)insertNewObject {

// Create a new instance of the entity managed by the fetched
   results controller.
  NSEntityDescription *entity =
      [[fetchedResultsController fetchRequest] entity];
  hotel = [NSEntityDescription
       insertNewObjectForEntityForName:[entity name]
       inManagedObjectContext:trip.managedObjectContext];
  [self setUpUndoManager];
  [hotel setValue:@"Hotel California" forKey:@"name"];
  [hotel setValue:@"600 Sutter Street" forKey:@"street"];
  [hotel setValue:@"San Francisco" forKey:@"city"];
  [hotel setValue:@"California" forKey:@"state"];
  [hotel setValue:@"94108" forKey:@"zip"];

  Annotation *annotation = [NSEntityDescription
       insertNewObjectForEntityForName:@"Annotation"
       inManagedObjectContext:trip.managedObjectContext];
  [annotation setTitle:@"Annotation"];
  [annotation setHotel:hotel];
  [hotel setAnnotation:annotation];

  Geocoder * geocoder = [[Geocoder alloc] init];
  NSString* geocodeString = [[NSString alloc ]
        initWithFormat: @" %@ %@ %@ %@",
        hotel.street, hotel.city, hotel.state, hotel.zip];
  CLLocationCoordinate2D theCoordinate =
                 [geocoder geocodeLocation:geocodeString];

  hotel.annotation.latitude =
      [NSNumber numberWithDouble: theCoordinate.latitude];
  hotel.annotation.longitude =
[NSNumber numberWithDouble: theCoordinate.longitude];
  hotel.annotation.title = hotel.name;
// Save the context.
  NSError *error = nil;
  if (![trip.managedObjectContext save:&error]) {
   // Replace this implementation with code to handle the
   // error appropriately.
  NSLog(@"Unresolved error %@, %@", error,
                                        [error userInfo]);
'Tony Bove' abort();
  }
  [trip loadHotels];
  [trip.mapController refreshAnnotations];
}

The modifications spelled out in Listing 5-15 do two things for you. First, you arrange it so that you send a message to yourself to set up the Undo manager.

[self setUpUndoManager];

With that done, you then delete the code you added to save the context and refresh the annotations. That's because you don't want to save or update anything until you're quite sure what the last Undo/Redo action was. I show you later where you'll do that save and update.

Now you will have to implement two methods in HotelController. Start by making the changes to HotelController.h shown in bold in Listing 5-16.

Example 5-16. HotelController.h

@class Trip;
@class Hotel;

@interface HotelController : UITableViewController
   <NSFetchedResultsControllerDelegate> {
  NSFetchedResultsController *fetchedResultsController;
  Trip                       *trip;
  Hotel                      *hotel;
  NSUndoManager              *undoManager;
}
@property (nonatomic, retain) NSFetchedResultsController
   *fetchedResultsController;
 @property (nonatomic, retain) NSUndoManager *undoManager;
- (void)setUpUndoManager;
- (void)cleanUpUndoManager;

@end

Here you have declared two methods to set up (and then clean up) the Undo manager and a property to hold a reference to the Undo manager.

Next, you need to add the two methods setUpUndoManager and cleanUpUndoManager shown in Listings 5-17 and 5-18, respectively, to TripController.m.

Example 5-17. Setting Up the Undo Manager

- (void)setUpUndoManager {
  if (hotel.managedObjectContext.undoManager == nil) {
    hotel.managedObjectContext.undoManager =
                                [[NSUndoManager alloc] init];
    [hotel.managedObjectContext.undoManager setActionName:
                   [NSString stringWithString:@"Add Hotel"]];

                      [hotel.managedObjectContext.undoManager
    setLevelsOfUndo:3];
      [hotel.managedObjectContext.undoManager release];
    }
   self.undoManager = hotel.managedObjectContext.undoManager ;

// Register as an observer of the hotel's context's undo
   manager.
  NSNotificationCenter *dnc =
                  [NSNotificationCenter defaultCenter];
 [dnc addObserver:self selector:@
   selector(undoManagerDidUndo:) name:NSUndoManagerDidUndoCha
   ngeNotification object:undoManager];
 [dnc addObserver:self
           selector:@selector(undoManagerDidRedo:) name:NSUnd
   oManagerDidRedoChangeNotification object:undoManager];
}

If the Hotel object's Managed Object Context doesn't already have an Undo manager, then create one and set it for the context and self.

if (hotel.managedObjectContext.undoManager  == nil) {
NSUndoManager *anUndoManager =
                           [[NSUndoManager alloc] init];

You also set what the Undo alert will display (it'll display Add Hotel) and the level of undo (up to 3 undos).

[anUndoManager setActionName:
              [NSString stringWithString:@"Add Hotel"]];
    [anUndoManager setLevelsOfUndo:3];

Keeping tabs on your Undo manager

The view controller needs to keep a reference to the Undo manager it creates so that it can determine whether to remove the Undo manager when editing finishes (in the case of a table view that supports editing) or when the view unloads (in our case). You also assign the newly created Undo manager to the Managed Object Context:

self.undoManager = anUndoManager;
    [anUndoManager release];
    hotel.managedObjectContext.undoManager = undoManager;

Then you register as an observer of the Hotel object's Managed Object Context's Undo manager. You tell the notification center to send the undo ManagerDidUndo: message to self when an undo occurs and the undo ManagerDidRedo: message when a redo occurs.

NSNotificationCenter *dnc =
                  [NSNotificationCenter defaultCenter];
 [dnc addObserver:self selector:@
   selector(undoManagerDidUndo:) name:NSUndoManagerDidUndoCha
   ngeNotification object:undoManager];
 [dnc addObserver:self
   selector:@selector(undoManagerDidRedo:) name:NSUndoManager
   DidRedoChangeNotification object:undoManager];
}

When you no longer need the Undo manager, you'll clean up after yourself. Enter the code in Listing 5-18 to HotelController.m.

Example 5-18. Cleaning Up the Undo Manager

- (void)cleanUpUndoManager {

// Remove self as an observer.
  [[NSNotificationCenter defaultCenter]
                                     removeObserver:self];

  if (hotel.managedObjectContext.undoManager ==
                                            undoManager) {
  hotel.managedObjectContext.undoManager = nil;
  self.undoManager = nil;
  }
}

You'll remove self as an observer and set the properties to nil.

You registered with the notification center to have a message sent whenever there was an undo or redo. Add the code in Listing 5-19 to HotelController.m to implement those methods.

Example 5-19. undoManagerDidUndo and undoManagerDidRedo

- (void)undoManagerDidUndo:(NSNotification *)
                                            notification {
  [self.tableView reloadData];
}


- (void)undoManagerDidRedo:(NSNotification *)
                                            notification {

  [self.tableView reloadData];
}

In both cases, you simply reload the table view to either remove (or add back) the entry.

Finally, a little housekeeping. The view controller must be first responder in order to be able to receive Shake events for undo. To do that, add the code in Listing 5-20 to HotelController.h.

Example 5-20. Setting the Correct First Responder Status

- (BOOL)canBecomeFirstResponder {

  return YES;
}


- (void)viewDidAppear:(BOOL)animated {

  [super viewDidAppear:animated];
  [self becomeFirstResponder];
}

Finally, when the view unloads and undos and redos are no longer possible, you need to save the current Managed Object Context and resign as first responder. Add the code in Listing 5-21 HotelController.m to send the cleanUpUndoManager message.

Example 5-21. viewWillDisappear:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];
  [self cleanUpUndoManager];
  // Save the context.
  if (trip.managedObjectContext.hasChanges) {
    NSError *error = nil;
    if (![trip.managedObjectContext save:&error]) {
  /*
   Replace this implementation with code to handle the error
   appropriately.
  */
      NSLog(@"Unresolved error %@, %@", error,
                                        [error userInfo]);
    abort();
    }
  }
  [trip loadHotels];
  [trip.mapController refreshAnnotations];
  [self resignFirstResponder];
}

The viewWillDisappear: message is sent to the view controller, well, when the view is going to disappear. This is the place where you want to commit the changes. First, you send the cleanUpUndoManager method you implemented in Listing 5-18. Then you save the context, and then send the loadHotels and refreshAnnotations messages to ensure that the hotel will appear on the map — this is simply the code you had in insertNewObject previously and does the same thing here. You also resign as first responder because you're ending the Undo/Redo cycle (and the view is disappearing anyway).

When you compile this code, go ahead and add a hotel using the Add button, as I have in Figure 5-5.

At this point, you can only add the hotel you hard coded in the insertNew Object method — but patience, you'll add the code and the user interface in Book VII, Chapter 1 that will allow the user to decide where he or she wants to stay.

Then either shake the device, or choose Hardware

viewWillDisappear:

If you touch Undo Add Hotel, your added hotel will be undone, as you can see in Figure 5-7.

Adding a hotel.

Figure 5-5. Adding a hotel.

Undo add hotel?

Figure 5-6. Undo add hotel?

The result of Undo Add Hotel.

Figure 5-7. The result of Undo Add Hotel.

Adding Delete

After you add a Hotel, it would also be nice if you could delete it if you change your mind. Normally, you'd do that using the Edit function in the table view or a Delete button when displaying the details of the Hotel. Although you don't have either of those available yet, I still want to show you what you need to do as far as Core Data is concerned to delete an object. To do that, you'll make the Hotel entry in the HotelController selectable, and when it's selected for an entry, you will delete it.

Don't worry, I show you how to deal with delete the right way in Book VII, when I show you how to enter the hotel data in a view and interface to your Address book.

Note

I want to point out that I won't be implementing undo for delete. I leave that as an exercise for you.

To start with, you need to modify the default tableView:didSelectRowAtIndexPath: method supplied in the CoreData template you pasted into your application in the previous section.

Make the modification shown in bold in Listing 5-22 to tableView:didSelectRowAtIndexPath: in HotelController.m. It was really just a stub with comments. I've omitted the comments and you can delete them if you would like.

Example 5-22. HotelController.m

- (void)tableView:(UITableView *)tableView didSelectRowAtInde
   xPath:(NSIndexPath *)indexPath {
  [tableView deselectRowAtIndexPath:indexPath
                                            animated:YES];
  hotel = [[self fetchedResultsController]
                              objectAtIndexPath:indexPath];
  [trip deleteHotel:hotel];
  hotel = nil;
}

The first thing you do in Listing 5-22 is deselect the row, and then get the Hotel object you're going to delete from the fetchedResultsController using the indexPath to determine which object that is. (The fetchedResultsController really does make things easier when you are using table view.)

[tableView deselectRowAtIndexPath:indexPath animated:YES];
hotel = [[self fetchedResultsController]
                             objectAtIndexPath:indexPath];

Then you send the deleteHotel: message to Trip, passing the Hotel you want to delete as an argument and setting the hotel instance variable to nil.

[trip deleteHotel:hotel];
hotel = nil;

Finally, you have to implement the deleteHotel: method. Add the code in Listing 5-23 to Trip.m.

Example 5-23. deleteHotel:

- (void) deleteHotel:(Hotel*) theHotel {

  [hotels removeObject:theHotel];
  [managedObjectContext deleteObject:theHotel];
  // Save the context.
  NSError *error;
  if (![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error
   appropriately.

    NSLog(@"Unresolved error %@, %@", error,
                                     [error userInfo]);
    abort();
  }
  [self loadHotels];
  if (self.mapController)  [self.mapController
   refreshAnnotations];
}

The first thing you do is remove the Hotel you're deleting for the hotels array:

[hotels removeObject:theHotel];

Then you remove the Hotel from the Managed Object Context:

[managedObjectContext deleteObject:theHotel];

Then you save the Managed Object Context:

if (![managedObjectContext save:&error]) {

    // Replace this implementation with code to handle the
   error appropriately.


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

Again, as I mention earlier, when it comes to production code, you shouldn't simply abort when your code confronts an error.

Lastly, you reload the (remaining) hotels and then send a message to the mapController to refresh the annotations.

[self loadHotels];
  if (self.mapController) [self.mapController
                                      refreshAnnotations];

You also have to add the following to Trip.h:

@class Hotel;

as well as the method declaration for deleteHotel:

- (void) deleteHotel:(Hotel*) theHotel;

What's Left

This chapter completes your introduction to Core Data, but there are some things you still need to add to to the RoadTrip application.

For one, you'd like to be able to enter the name and address of the Hotel, instead of always having to stay at the same place. It would also be nice if you could save all that information as a contact in your Address Book. Conversely, if the information is already in your Address Book, you'd like to use that for the Hotel.

When you're on the road, you'd also like to send e-mail about where you are.

And you might like to know, at any point in time, by simply shaking your phone, how far you are from Apple Computer headquarters. (Others would like to know how far you are from a destination or sight.)

I cover all this in Book VI.

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

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