Chapter 17
Core Data

Key Skills & Concepts

• Understanding Core Data’s basics

• Creating a Core Data model

• Understanding how to load, fetch, and save a model’s data

• Building a complex navigation-based application using Core Data

• Knowing where to obtain more Core Data information

With its addition to the iOS SDK, Core Data is arguably the best choice for persisting an application’s data. It is more robust than using properties and is easier than using SQLite. You can visually lay out your application’s data model, much as you would when using a database-modeling tool. Moreover, it provides the infrastructure for managing the objects you create, freeing you from writing the typical object management code. As this chapter will demonstrate, using Core Data is a natural choice for persisting an application’s data. Core Data allows you to focus on the application rather than on the code persisting the application’s data.

In previous chapters, the Try This examples were generally very small applications that illustrated a single concept but would never be mistaken for a full iOS application that you might find in the App Store. In this chapter, we’re going to build a more complex reference application. By the end of this chapter, you’ll have a fairly useful application that, while not quite ready for the App Store, just needs a little more data and some polish. In Chapter 19 we’ll take the result of this chapter and turn it into a universal application that will also run native on the iPad with its larger screen.

Since the application we are building will have several views and build on skills that you learned in earlier chapters, in the interest of readability, we will only provide complete in-line code listings where we are illustrating new functionality. Before getting started on this chapter, it will be helpful to download the DogBreeds-Final sample code for this chapter. With the completed sample application code handy, you can reference it when you’re not sure how to do a particular step.

Core Data in Brief

Core Data is a framework used to easily manage an application’s data objects. Core Data consists of managed object models, managed object contexts, and persistent data stores.

A managed object model contains an object graph. The object graph is a collection of objects and their relationships with one another. You create a graph using Xcode’s data modeler. The data modeler is where you visually add entities and create relationships between them.

Image

Figure 17-1 Core Data’s architecture (simplified)

A managed object context contains the objects created from the entities in the managed object model. A managed object context has a persistent store coordinator that manages one or more persistent stores. A persistent data store persists objects created by the managed object context. Although an application can have multiple persistent stores, in this chapter, you restrict yourself to one persistent store. Figure 17-1 illustrates Core Data’s architecture.

Creating a Model

A model contains entities. Entities contain attributes. Relationships model how one or more entities relate to one another. You model these concepts using Xcode’s data modeler. You add a model to your application by creating a file with an .xcdatamodel extension through Xcode’s New File dialog (Figure 17-2). It is best to place the model file in a new Models folder. After creating the model, when you select the file, Xcode should automatically display the data modeler in the Editor window.

Entities

Entities, represented by the NSEntityDescription class, are patterns describing NSManagedObject instances. NSManagedObjects are what you persist. For Core Data to know how to instantiate new object instances and persist the instances, it needs a pattern. Entities in the model provide those patterns. For instance, you might want to create a handy reference application that lists all of the American Kennel Club (AKC) dog breeds. The AKC divides all of the dog breeds into categories like “herding group” and “sporting group,” so we’ll need an AKCGroup class to represent these groups. Within each group there are a number of dog breeds, so we also have a Breed class to represent the breeds. These straightforward Objective-C classes will be subclasses of NSManagedObject, and you tell Core Data how to persist them using entities.

Image

Figure 17-2 Creating a new Core Data model

Attributes

Entities tell Core Data which classes we will need to persist. In order to actually save objects in a persistent store and reconstitute them later, Core Data also needs to know what information must be saved for each object. Therefore, in our data model, entities have attributes that describe each of the properties that will need to be persisted.

Relationships

In all but the simplest of applications, there will also be relationships between the classes you define. For instance, each AKCGroup will contain one or more Breed objects and every Breed will be in exactly one group. The objects were modeled with entities, and how they are interrelated is modeled with relationships. Entities can have relationships with other entities. For instance, an apple and an orange are both types of fruit, a crate might hold one or more apples and oranges, and a fruit stand might contain one or more crates.

Try This
Adding Entities and Relationships to a Core Data Model

Reference applications are particularly popular in the App Store. They typically contain hierarchical information that is easily viewed with a navigation-based iOS application. In fact, this pattern is so common that Xcode provides a template that creates a navigation-based application that is tied to a Core Data model. In this first task, you will first create a new navigation-based application and tell Xcode that it uses Core Data. Then you create the entities needed for a dog breeds reference application and add attributes and relationships. Later tasks will generate Objective-C classes that correspond to those entities, and then write the code to create, delete, and navigate those objects.

1. Create a new Navigation-based Application named DogBreeds. Be certain to select the Use Core Data check box (Figure 17-3). Xcode will create an almost functional Navigation-based Application with Core Data support, including a default xcdatamodel file.

Image

Figure 17-3 Creating a new Navigation-based Application

2. The default xcdatamodel contains a single Event entity. If there were corresponding Event.h and Event.m class files for the Event entity, you would be able to run the new application that was just created. Fortunately, Xcode will create the class definition for you based on the attributes defined in the xcdatamodel. Select the Entity and then select Create NSManagedObject Subclass from the Editor menu.

3. Event.h and Event.m will be generated (Listings 17-1 and 17-2). Run the application. You will notice that you can create new “events” in the list, and edit the list to delete events. If you quit the application and later relaunch it, the events have all been persisted.

4. Take a few minutes to look at the generated source code for the AppDelegate and RootViewController and familiarize yourself with the methods that were automatically generated for you.

Listing 17-1 Definition of a simple Event class

#import <Cocoa/Cocoa.h>
@interface Event : NSManagedObject {
@private
}
@property (nonatomic, retain) NSDate * timeStamp;
@end

Listing 17-2 Implementation of a simple Event class

#import "Event.h"
@implementation Event
@dynamic timeStamp;
@end

5. Now we’ll change the default application to reflect our dog breeds data model. Delete the Event.h and Event.m files. Select the DogBreeds.xcdatamodel file. Delete the Event entity by selecting it and pressing the DELETE key.

6. Click the Add Entity button (at the bottom of the pane) and create a new entity called AKCGroup (Figure 17-4).

7. Click the Add Attribute button and add an attribute called groupDescription with type String. Create another attribute called name, also with type String.

8. Click the Add Entity button again and create a new entity called Breed. Give the Breed entity three String attributes: name, breedDescription, and photoURL (Figure 17-5).

Image

Figure 17-4 Adding the AKCGroup entity

9. With the Breed entity selected, click and hold on the Add Attribute button and select Add Relationship. Name the new relationship group and select AKCGroup from the Destination pull-down menu.

10. Select View | Utilities | Core Data Model to display the relationship details. Uncheck the Optional check box.

11. Select the AKCGroup entity and add a new relationship to it called breeds. Set its destination to Breed and Inverse to group. Select the To-Many Relationship check box and set the delete rule to Cascade (Figure 17-6).

12. Save the model. You are finished describing the objects we need to persist and how they relate to each other. Core Data will use this information to determine how and when to save your objects in an underlying SQLite database.

13. Now you need to create the underlying classes for the AKCGroup and Breed entities. Highlight AKCGroup and Breed and then select Editor | Create NSManagedObject Subclass from the main menu.

Image

Figure 17-5 Breed entity with its attributes

14. Xcode will generate four files for you, Breed.h, Breed.m, AKCGroup.h, and AKCGroup.m, based on the entity descriptions we created earlier.

15. Select Breed.h and take a look at what was generated for you. You will notice that the Breed class has properties defined for each of the attributes in the entity. You will also notice that the Breed class doesn’t contain instance variables for the properties. Instead, for Core Data managed classes, they are defined with the @dynamic directive in the implementation file.

NOTE
NSManagedObjects have no dealloc methods, as the Core Data framework manages their life cycle. Core Data is also responsible for generating NSManagedObjects’ accessor methods at runtime.

16. Select the AKCGroup.h file and take a look at what was generated for you. There are properties for the two attributes and the relationship that we defined in the xcdatamodel file.

Image

Figure 17-6 Relationship between AKCGroup and Breeds

17. We have changed the object model since you ran the default application in Step 2, so you need to completely delete the application from the iPhone Simulator so that the database containing the Event data will also be deleted. Otherwise, when you run the modified application in the next task, you will get an error because the model changed from Event entities to Breeds and AKCGroups.

Notice you specified that the relationship between AKCGroup and Breed has a Cascade Delete Rule. This rule informs the model that when an AKCGroup object is deleted, then any Breed objects that it references should also be deleted. Other delete rules you might specify include Nullify, Deny, and No Action.

NOTE
For more information on creating models using Xcode, refer to Apple’s “Creating a Managed Object Model with Xcode.”

Model, Context, and Store

The preceding task created the model used to create entities and their relationships in a managed object context. When an application runs, it needs a model instance, the context, and the persistent store. The persistent store and the model are largely transparent to you when coding your application. You simply obtain both these items and then set properties referencing them in the managed object context. Because we started from a navigation-based application template set to use Core Data, Xcode added code to create and manage these for us.

NSManagedObjectModel

As discussed earlier, an application’s managed object model contains entities and their relationships. It serves as an application’s schema by describing the entities used in an application’s managed object context. Take a look at DogBreedsAppDelegate and you will see that Xcode added a property to the AppDelegate to hold the NSManagedObjectModel and an accessor method that creates the managedObjectModel on the first access. The easiest way to obtain the model is through the mergedModelFromBundles: class method.

managedObjectModel =
        [[NSManagedObjectModel mergedModelFromBundles: nil] retain];

This method creates a data model by merging all the models it finds into a bundle. Because the previous code specifies nil, the method simply finds all the models in the application’s Resources folder and merges them into one NSManagedObjectModel instance.

NSPersistentStoreCoordinator

A persistent store coordinator coordinates one or more persistent stores and associates them with a managed object model. While the managed object model defines what gets persisted, the persistent store takes care of the low-level details of how and where the data is actually saved. Although advanced applications might have more than one persistent store, this chapter limits consideration to single-store applications. Core Data can persist data in several different ways. Store types you might use to persist data include NSSQLiteStoreType, NSBinaryStoreType, or NSInMemoryStoreType. For most purposes, you’ll want to use a SQLite database for the Persistent Store and that is what Xcode created in the persistentStoreCoordinator accessor in DogBreedsAppDelegate.

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

After obtaining the URL to the store, it creates an NSPersistentStoreCoordinator instance using the managed object model instance. The persistent store contains the data, while the model defines how to interpret that data.

persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
        initWithManagedObjectModel: [self managedObjectModel]];
NSError *error = nil;
[persistentStoreCoordinator
          addPersistentStoreWithType: NSSQLiteStoreType
          configuration: nil
          URL: storeUrl options:nil error:&error]

NSManagedObjectContext

The NSManagedObjectContext represents an application’s managed object context, or the object instances (NSManagedObject classes) that your application is manipulating. Managed objects are fetched from the persistent store into the NSManagedObjectContext, and it is where you modify the objects. Apple describes the context as a big “scratch-pad” because no manipulations to a context are persisted until code explicitly tells the context to persist the changes.

Xcode added a managedObjectContext property to DogBreedsAppDelegate and an accessor that creates the context on first access. Obtain an application’s managed context by allocating and initializing a new NSManagedObjectContext instance. You then set its persistent store coordinator.

managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];

NSManagedObject

The NSManagedObjectContext manages NSManagedObject instances. NSManagedObjects are not entities, but rather, created from entities. An application obtains data from the persistent store and uses the entities in the model to create the NSManagedObjects placed in the context. Consider NSEntityDescriptions as the classes and NSManagedObjects as the objects.

The previous Try This task created entities in the xcdatamodel file. Although the NSManagedObjectModel uses these entities, the NSManagedObjectContext does not; it manages NSManagedObjects. Our AKCGroup and Breed classes are therefore subclasses of NSManagedObject.

NSFetchedResultsController

As a navigation-based application, the DogBreeds application will use a hierarchy of UITableViews to display our AKC groups and breeds. Displaying objects from Core Data in UITableViews is such a common task that iOS provides support for easily connecting fetched results to a table view. The NSFetchedResultsController object and NSFetchedResultsControllerDelegate protocol make it relatively easy to retrieve a set of objects from Core Data and modify the set as the user adds, deletes, and updates them via a table view.

NSFetchRequest

The NSFetchRequest class is how you query a persistent object store for its data. It uses an NSEntityDescription to know which entity to fetch. Listing 17-3 illustrates creation of an NSFetchRequest and an NSEntityDescription, and assigns the description to the request. The NSManagedObjectContext then executes the request and returns the matching objects in an NSArray.

Listing 17-3 NSFetchRequest example

NSFetchRequest * myRequest = [[NSFetchRequest alloc] init];
NSEntityDescription * entDesc = [NSEntityDescription
        entityForName:@"AKCGroup" inManagedObjectContext:myContext];
[myRequest setEntity:entDesc];
NSError * error;
NSArray * fetchResults = [self.managedObjectContext
executeFetchRequest:myRequest
error:&error];
if(fetchResults == nil) {
  NSLog(@"an error occurred");
  [error release];
}

Notice Listing 17-3 selects all the AKCGroups in myContext. Unless you know there will always be a reasonable number of results, you will want to limit the results returned. One way you could do this is through the NSFetchRequest’s fetchLimit property. This property limits the objects returned by a fetch request. However, this property does not distinguish which objects to exclude. Often, you will want to limit results to only objects meeting certain criteria. For instance, you might want all Breeds in a particular AKCGroup. The way you limit results based upon given criteria is through the NSPredicate class.

NSPredicate

The NSPredicate class restricts the data returned by an NSFetchRequest. It is similar to a SQL statement’s WHERE clause. The easiest way to create a predicate is by using the predicateWithFormat class method.

+ (NSPredicate *)predicateWithFormat:(NSString *)format,

The code is similar to initializing an NSString with a format. You write the expression and include a substitution parameter, followed by one or more substitution values. For instance, you might create a predicate limiting Breeds to those with a particular group.

NSPredicate *groupFilter =
        [NSPredicate predicateWithFormat:
           @"group = %@", self.selectedGroup];

Notice the preceding predicate does not tell you which entity the predicate is associated with; to make the association, you set the entity and predicate in the same fetch request.

[myRequest setEntity:entDesc];
[myRequest setPredicate: groupFilter];

Predicates can have more than one item in the substitution list. For instance, you might create the following predicate:

NSPredicate * predicate = [NSPredicate predicateWithFormat:
@"group = %@ and hairLength like %@", self.selectedGroup, @"short"];

This predicate assigns a specific AKCGroup to the group value and short to the hairLength value. Notice the “like” keyword; there are many similarities between Apple’s predicate syntax and SQL.

NOTE
Apple’s predicate syntax is quite detailed. For more information on predicate syntax, see Apple’s “Predicate Programming Guide.”

NSSortDescriptor

By default, fetched objects are unsorted; sorting the objects requires an NSSortDescriptor instance. This class represents the sort order for a fetched object collection. The following statement creates and initializes an NSSortDescriptor that sorts AKCGroups in ascending order based upon their name:

NSSortDescriptor * myDesc = [[NSSortDescriptor alloc]
initWithKey:@"name" ascending:YES];

A request can have more than one sort descriptor, so you add your NSSortDescriptors to an NSArray and then add the array to the NSFetchRequest using its setSortDescriptors method if you want to sort by multiple values.

NOTE
Although the topic is not covered in this chapter, you can predefine fetch requests, predicates, and sort descriptors in an application’s xcdatamodel file. Then at runtime, you can fetch those predefined objects from the data model and use them in your code.

If you take a look at the RootViewController class that Xcode automatically generated for us, you’ll see a property to store the NSFetchedResultsController and an accessor that instantiates the controller the first time it’s accessed. In the default template the accessor fetches Event objects, so our next task is to change it to retrieve and manipulate the AKCGroup objects at the top of our hierarchy.

Try This
Fetching All AKCGroup Entities

1. Open DogBreeds in Xcode.

2. Open RootViewController.m and modify the NSEntityDescription to change it from Event to AKCGroup. Change the NSSortDescriptor to sort on name in ascending order and comment out the limit on the fetch, since we know there will never be more than a few AKC groups (Listing 17-4).

Listing 17-4 fetchedResultsController Accessor in RootViewController

- (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:@"AKCGroup"
          inManagedObjectContext:self.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:@"name"
ascending:YES];
  NSArray *sortDescriptors =
        [[NSArray alloc] initWithObjects:sortDescriptor, nil];
  [fetchRequest setSortDescriptors:sortDescriptors];
  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];
  NSError *error = nil;
  if (![[self fetchedResultsController] performFetch:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }
  return fetchedResultsController;
}

3. The default implementation of RootViewController also refers to the timestamp attribute of Event in a couple of places, so we’ll need to change those references as well before we can run our application. Find the insertNewObject method in RootViewController and change it to set the AKCGroup name and groupDescription instead of the Event’s timestamp (Listing 17-5).

Listing 17-5 insertNewObject in RootViewController

- (void)insertNewObject {
  // Create a new instance of the entity managed by the
  // fetched results controller.
  NSManagedObjectContext *context =
        [fetchedResultsControllermanagedObjectContext];
  NSEntityDescription *entity =
        [[fetchedResultsController fetchRequest] entity];
  NSManagedObject *newManagedObject =
        [NSEntityDescription insertNewObjectForEntityForName:
        [entity name] inManagedObjectContext:context];
  [newManagedObject setValue:@"A new group" forKey:@"name"];
  [newManagedObject setValue:@"Description of group"
       forKey:@"groupDescription"];
  // 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();
  }
}

4. Modify the configureCell method in RootViewController so that it displays the AKCGroup’s name and description in the cell instead of the timestamp from the Event entity (Listing 17-6).

Listing 17-6 configureCell in RootViewController

- (void)configureCell:(UITableViewCell *)cell
        atIndexPath:(NSIndexPath *)indexPath {
  NSManagedObject *managedObject = [self.fetchedResultsController
          objectAtIndexPath:indexPath];
  cell.textLabel.text = [managedObject valueForKey:@"name"];
  cell.detailTextLabel.text = [managedObject
          valueForKey:@"groupDescription"];
}

5. Save your changes and run the application. Notice that with hardly any effort, you now have an application that displays a list of AKCGroups and lets you add and delete groups from that list (Figure 17-7).

Image

Figure 17-7 Adding and deleting AKCGroups

Adding Objects

All objects managed by a managed object context are NSManagedObject instances. NSManagedObject is a class that implements the required behavior for a Core Data model object. You do not create NSManagedObject instances, but rather subclasses. These subclasses are usually created from the entities defined in an xcdatamodel file.

The easiest way to create a new managed object is through the NSEntityDescription’s class method, insertNewObjectForEntityForName:inManagedObjectContext.

+ (id)insertNewObjectForEntityForName:(NSString *) entityName
inManagedObjectContext:(NSManagedObjectContext *) context

This method obtains an entity from the model, creates a new NSManagedObject based upon the entity, and inserts it in the current managed object context. For instance, the following code from insertNewObject in Listing 17-5 creates a new AKCGroup from the AKCGroup entity used in this chapter’s xcdatamodel file:

AKCGroup * newGroup = (AKCGroup *) [NSEntityDescription
        insertNewObjectForEntityForName: @"AKCGroup"
        inManagedObjectContext:self.managedObjectContext];

After inserting a new object, you can then set its properties, just as if it were a normal object. The following code sets the newly created AKCGroup’s name:

newGroup.name = @"A new group";

Saving Changes

An application’s managed object context does not automatically save changes to a model’s data. You must manually save the context to persist changes. For instance, when an application is suspended or terminates, you might want to check the context for changes and, if there were changes, save them.

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

The context saves changes using its save method. This method persists the context’s changes to its associated persistent data store. The method takes an error as a parameter and returns a Boolean indicating success or failure.

- (BOOL)save:(NSError **) error

You can also roll back all changes to a context using the rollback method. This method removes everything from something called the undo stack and removes all insertions and deletions, and restores all context-managed objects to their original state.

NOTE
An NSManagedObjectContext can have an NSUndoManager instance assigned to its undoManager property. An NSUndoManager manages undoing actions. When using Core Data, you can use this class to undo changes made to an application’s NSManagedModelContext. For more information, refer to the NSUndoManager Class Reference.

Deleting Entities

You use the NSManagedObjectContext’s deleteObject method to delete objects from an application’s managed object context. This method takes an NSManagedObject instance of the object to delete. For instance, the default code in tableView:commitEditingStyle deletes an AKCGroup for us with the following code:

    NSManagedObjectContext *context = [self.fetchedResultsController
managedObjectContext];
    [context deleteObject:[self.fetchedResultsController
objectAtIndexPath:indexPath]];
    // Save the context.
    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

Note that the managed object context marks the particular AKCGroup for deletion with the deleteObject call, but it doesn’t make the actual change to the persistent store until you call the context’s save method.

Updating Entities

Modifying an object from a managed object context is as easy as simply changing its properties and then saving the context. In the next task we’ll create a subview for editing an AKCGroup, and when the user taps the Done button, we’ll just update the properties of the group from the fields in the view and then save the context with code like the following.

self.group.name = nameField.text;
self.group.groupDescription = groupDescriptionField.text;
NSError *error = nil;
if (![self.group.managedObjectContext save:&error]) {
    // Be sure to handle any errors
}

Try This
Adding Navigation and AKCGroup Editing

1. Open DogBreeds in Xcode.

2. The Xcode template automatically created an Edit button on the left and an Add button on the right for us. Since we’re going to eventually extend our application to have another level of navigation (select a group to see all of the breeds in that group), we will need to use the left button for returning back up a level. Open RootViewController.m and change the viewDidLoad method to put the Edit button on the right instead of the Add button (Listing 17-7). Also give the view an appropriate title.

Listing 17-7 viewDidLoad in RootViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Set up the edit button.
    self.navigationItem.rightBarButtonItem = self.editButtonItem;
    self.title = @"AKC Groups";
}

3. When the user taps on the Edit button to edit the list of AKC groups, we want to display the Add button so that they can add new groups as well as delete or edit groups. When the user taps on the Edit button in a UITableView, a setEditing message is sent to the UITableView’s delegate (our RootViewController), so we can implement that method and create or remove the Add button when they go into and out of the edit screen (Listing 17-8).

Listing 17-8 setEditing in RootViewController

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    [super setEditing:editing animated:animated];
    //[tableView setEditing:editing animated:YES];
    if (editing) {
        UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self
action:@selector(insertNewObject)];
        self.navigationItem.leftBarButtonItem = addButton;
        [addButton release];
    } else {
        self.navigationItem.leftBarButtonItem = nil;
    }
}

Image

Figure 17-8 AKCGroups table in editing mode

4. Save your changes and run the application. Notice that the Add button now shows up after we’ve started to edit the group list, but it still works the way it did before (Figure 17-8).

5. Now we need to create a new view to actually edit the AKCGroup’s properties. Add an IBOutlet for the new view in RootViewController.h (Listing 17-9). Don’t forget to synthesize the property in RootViewController.m and release it in the dealloc method.

Listing 17-9 RootViewController.h

#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@class AKCGroupViewController;
@interface RootViewController : UITableViewController
<NSFetchedResultsControllerDelegate> {
NSFetchedResultsController *fetchedResultsController;
    NSManagedObjectContext *managedObjectContext;
    AKCGroupViewController *groupEditorVC;
}

@property (nonatomic, retain) NSFetchedResultsController
*fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext
*managedObjectContext;
@property (nonatomic, retain) IBOutlet AKCGroupViewController
*groupEditorVC;
@end

6. Create a new UIViewController subclass called AKCGroupViewController with an XIB file. Edit the header file and add IBOutlets for the name and groupDescription fields. Also add properties to store the AKCGroup that we’re editing and whether or not it’s a group insertion (Listing 17-10).

Listing 17-10 AKCGroupViewController.h

#import <UIKit/UIKit.h>
@class AKCGroup;
@interface AKCGroupViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate> {
    AKCGroup *group;
    UITextField *nameField;
    UITextView *groupDescriptionField;
BOOL insertingGroup;
}
@property(nonatomic, retain) IBOutlet UITextField *nameField;
@property(nonatomic, retain) IBOutlet UITextView *groupDescriptionField;
@property(nonatomic, retain) AKCGroup *group;
@property(nonatomic, assign) BOOL insertingGroup;
@end

7. Select AKCGroupViewController.xib and add a label and text field for the group name and a label and text view for the description. Connect them to the IBOutlets you created in Step 6 (Figure 17-9).

8. Open RootViewController.xib, create a new view controller, set its class to AKCGroupViewController, and set it to load from that nib. Connect the groupEditorVC outlet from the File’s Owner to the new view. Select the table view and check the Allows Selection While Editing option. Save your changes.

Image

Figure 17-9 Outlets for AKCGroupViewController

9. Open AKCGroupViewController.m and in the viewWillAppear method copy the group name and description from the group to the fields we just created in the XIB file. In the viewDidLoad method you will need to create two navigation buttons: put a Done button on the right and a Cancel button on the left (Listing 17-11).

10. Implement a done method that will be called when the Done button is pressed. It should copy the name and description back into the group object, ask the managedObjectContext to save changes and then pop the view (Listing 17-11).

11. Implement a cancel method that pops the view without saving any changes. If the insertingGroup flag was set, then this was a new group and cancel should delete the group object and ask the managedObjectContext to save changes (Listing 17-11).

Listing 17-11 AKCGroupViewController.m

#import "AKCGroupViewController.h"
#import "AKCGroup.h"
@implementation AKCGroupViewController
@synthesize nameField;
@synthesize groupDescriptionField;
@synthesize group;
@synthesize insertingGroup;

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
     self.nameField.text = self.group.name;
    self.groupDescriptionField.text = self.group.groupDescription;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    UIBarButtonItem *doneButton = [[UIBarButtonItem alloc]
           initWithBarButtonSystemItem: UIBarButtonSystemItemDone
          target:self action:@selector(done)];
    self.navigationItem.rightBarButtonItem = doneButton;
     [doneButton release];
    UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc]
          initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
          target:self action:@selector(cancel)];
     self.navigationItem.leftBarButtonItem = cancelButton;
    [cancelButton release];
}
- (void)viewDidUnload {
    [super viewDidUnload];
     // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}
- (void)done {
    self.group.name = nameField.text;
     [self.nameField resignFirstResponder];
    self.group.groupDescription = groupDescriptionField.text;
     [self.groupDescriptionField resignFirstResponder];
     NSError *error = nil;
     if (![self.group.managedObjectContext save:&error]) {
        // Be sure to handle any errors
    }
    [self.navigationController popViewControllerAnimated:YES];
}
- (void)cancel {
     [self.nameField resignFirstResponder];
     [self.groupDescriptionField resignFirstResponder];
    // If this was a new group that was created, then
     //cancel should get rid of the empty group from the database
    if (insertingGroup == YES) {
        // Delete the managed object for the given index path
        NSManagedObjectContext *context = self.group.managedObjectContext;
        [context deleteObject:self.group];
        // Save the deletion
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"Failed to save to data store: %@",
                      [error localizedDescription]);
            NSArray* detailedErrors = [[error userInfo]
                              objectForKey:NSDetailedErrorsKey];
            if(detailedErrors != nil && [detailedErrors count] > 0) {
                for(NSError* detailedError in detailedErrors) {
                    NSLog(@"  DetailedError: %@", [detailedError userInfo]);
                }
             }
            else {
                NSLog(@"  %@", [error userInfo]);
             }
             abort();
        }
    }
    [self.navigationController popViewControllerAnimated:YES];
}
- (void)dealloc {
     [group release];
    [nameField release];
    [groupDescriptionField release];
     [super dealloc];
}
@end

12. Finally, we need to modify insertNewObject in RootViewController and add a few lines at the end to push the group editing view when the user inserts a new group.

self.groupEditorVC.group = (AKCGroup *)newManagedObject;
self.groupEditorVC.insertingGroup = YES;
[self.navigationController pushViewController:self.groupEditorVC
animated:YES];

13. When the user taps on a row in a UITableView, the didSelectRowAtIndexPath method is called. Modify that method in RootViewController so that when the user taps on a group while in editing mode, the group editing view is also pushed.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
(NSIndexPath *)indexPath {
    AKCGroup *theGroup = [[self fetchedResultsController]
objectAtIndexPath:indexPath];
    if (self.editing == YES) {
        self.groupEditorVC.group = theGroup;
        self.groupEditorVC.insertingGroup = NO;
        [self.navigationController pushViewController:
                                self.groupEditorVC animated:YES];
    }
}

14. Save all of your changes and run the application. When you put the group list into editing mode, you can now click a group to edit it. When you create a new AKCGroup, you’re now immediately taken to the edit view (Figure 17-10). You can polish the view layout and some of the settings like field capitalization, but at this point you have a fairly functional application for manipulating a list of AKC groups.

Image

Figure 17-10 Edit view for AKCGroup

Navigation

You now have a fairly complete application for displaying, adding, deleting, and editing a list of AKC groups. But the groups alone aren’t too interesting. When the user taps on an AKC group while not in editing mode, she should see a new UITableView with all of the breeds within that group. Then if she taps on a breed in that list, she should see a detailed view of the breed with a photo and description.

The UITableView with the list of breeds within a group is going to be very similar to the list of AKC groups, so for the Try This task in this section you can largely copy the code from RootViewController and AKCGroupViewController and we will only highlight the differences.

Try This
Adding Navigation and Editing for a List of Breeds

1. Reopen DogBreeds in Xcode. If you have the DogBreeds-Final project downloaded, you might want to also open it so that you can easily refer to the final project if you are uncertain how to complete a step.

2. Using File | New File from the main menus, create a new subclass of UITableViewController (select the check boxes for a subclass of UITableViewController and create a xib file). Call it BreedsListViewController.

3. Edit BreedsListViewController.h and add a property to store which group was selected and IBOutlets for sub-views to edit a breed and display breed details. Also add properties for the fetchedResultsController and managedObjectStore like RootViewController. The class will also have to implement the NSFetchedResultsControllerDelegate protocol (Listing 17-12).

Listing 17-12 BreedsListViewController.h

#import <UIKit/UIKit.h>
@class AKCGroup;
@class BreedViewController;
@class BreedDetailViewController;
@interface BreedsListViewController :
                    UITableViewController
<NSFetchedResultsControllerDelegate> {
    AKCGroup *selectedGroup;
    BreedViewController *breedEditorVC;
    BreedDetailViewController *breedDetailVC;
    NSFetchedResultsController *fetchedResultsController;
    NSManagedObjectContext *managedObjectContext;
}
@property (nonatomic, retain) AKCGroup *selectedGroup;
@property (nonatomic, retain) IBOutlet BreedViewController
*breedEditorVC;
@property (nonatomic, retain) IBOutlet BreedDetailViewController
*breedDetailVC;
@property (nonatomic, retain) NSFetchedResultsController
*fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext
*managedObjectContext;

@end

4. Open BreedsListViewController.m and synthesize the properties you added in Step 3 and be sure to release them in the dealloc method.

5. Copy the fetchedResultsController accessor and all of the NSFetchedResultsControllerDelegate methods from RootViewController, since we’ll be fetching the Breed objects for the table in the same way as we did the AKCGroup objects in RootViewController. We do need to change the fetchedResultsController so that it retrieves Breed entities and only those entities in the selected group (using a predicate):

NSEntityDescription *entity =
        [NSEntityDescription entityForName:@"Breed"
        inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *groupFilter = [NSPredicate predicateWithFormat:
        @"group = %@", self.selectedGroup];
[fetchRequest setPredicate:groupFilter];

6. Change viewDidLoad to add the Edit button just like RootViewController. Copy the numberOfSectionsInTableView, numberOfRowsInSection, cellForRowAtIndexPath, and configureCell methods unchanged from RootViewController. Change configureCell to only display the name. Save your changes.

7. Edit RootViewController and add an IBOutlet called breedsListVC for the BreedsListViewController. Modify didSelectRowAtIndexPath so that when not in editing mode, it initializes the new breeds list controller and pushes it onto the view stack:

else {
  self.breedsListVC.selectedGroup = theGroup;
  self.breedsListVC.title = theGroup.name;
  self.breedsListVC.managedObjectContext =
        self.managedObjectContext;
  [self.navigationController
        pushViewController:self.breedsListVC
        animated:YES];
}

8. The BreedsListViewController object is created once, stored in the breedsListVC property of the RootViewController, and then reused as the user taps on different groups to view them. The fetchedResultsController accessor is designed to create the NSFetchedResultsController the first time and then just reuse it. This creates an interesting problem. The second time the user taps on a group, the didSelectRowAtIndexPath method in RootViewController changes the selected group in breedsListVC and changes the view’s title. But the fetchedResultsController in BreedsListViewController has already fetched results, and it is simply reused. You can solve this problem by overriding the default setter for selectedGroup and making the other necessary changes within the view when the group changes (Listing 17-13).

Listing 17-13 Overriding the setter for selectedGroup

- (void)setSelectedGroup:(AKCGroup *)theGroup {
    if(theGroup != self.selectedGroup) {
        [self.selectedGroup release];
        selectedGroup = [theGroup retain];
        self.fetchedResultsController = nil;
        [self.tableView reloadData];
    }
}

9. Save your changes and select RootViewController.xib. Add a ViewController for BreedsListViewController, set its class and NIB, and connect it to the breedsListVC IBOutlet.

10. Save your changes and try running the application. You should be able to tap on an AKCGroup and switch to a new table view with the group’s name as the title. The table is empty but would be displaying the breeds in that group if there were any. We can now navigate into and out of the AKC groups.

11. You can now implement the breed editing functionality exactly the same way we did it for the AKCGroup in RootViewController. You’ll need to define a BreedViewController class that lets you edit the breed’s name, photo URL, and description in exactly the same way you implemented AKCGroupViewController and change didSelectRowAtIndexPath to push it on the view stack when the user taps a row while in editing mode.

12. You will need to add an insertNewBreed method that is very similar to the insertNewAKCGroup method in RootViewController. However, in addition to creating the new Breed entity, it will also need to add it to the selected AKCGroup with addBreedsObject so that Core Data can maintain the relationship between them (Listing 17-14).

Listing 17-14 insertNewBreed in BreedsListViewController

- (void)insertNewBreed {

     NSManagedObjectContext *context = [self.fetchedResultsController
                                  managedObjectContext];
    Breed *newBreed = [NSEntityDescription insertNewObjectForEntityForName:
                                 @"Breed" inManagedObjectContext:context];
    [newBreed setValue:@"" forKey:@"name"];
     [newBreed setValue:@"" forKey:@"breedDescription"];
    [newBreed setValue:@"" forKey:@"photoURL"];
    [self.selectedGroup addBreedsObject:newBreed];

    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    self.breedEditorVC.breed = newBreed;
    self.breedEditorVC.insertingBreed = YES;
    self.breedEditorVC.selectedGroup = self.selectedGroup;
     [self.navigationController pushViewController:self.breedEditorVC
animated:YES];
}

13. Select BreedViewController.xib and add labels and text fields for the breed name and photo URL. Add a label and text view for the description. Connect the fields to the IBOutlets in the File’s Owner. Save your changes and then switch to BreedsListViewController.xib, add a view controller and change its class to BreedViewController, set it to load from the BreedViewController nib, and connect it to the breedEditorVC IBOutlet.

14. Save all of your changes and run the application. You can now add/edit/delete AKC groups, move in/out of AKC groups, and add/edit/delete breeds within any group (Figure 17-11). You’re getting close to a completed application.

Image

Figure 17-11 Editing screens for AKCGroup and Breed

Try This
Adding a Breed Detail View

1. Reopen DogBreeds in Xcode and create a new UIViewController subclass called BreedDetailViewController with a xib file.

2. Open BreedDetailViewController.h and add a property to store the selected breed. Add properties and IBOutlets for the breedDescription and photo (Listing 17-15). Save your changes and open BreedDetailViewController.m and synthesize the properties and release them in the dealloc method.

Listing 17-15 BreedDetailViewController.h

#import <UIKit/UIKit.h>
@class Breed;
@interface BreedDetailViewController : UIViewController {
    UITextView *breedDescription;
    UIImageView *photo;
    Breed *selectedBreed;
}
@property(nonatomic, retain) IBOutlet UITextView *breedDescription;
@property(nonatomic, retain) IBOutlet UIImageView *photo;
@property(nonatomic, retain) Breed *selectedBreed;
@end

3. Select BreedDetailViewController.xib and add UIImageView and UITextView objects. Connect them to the IBOutlets in the File’s Owner.

4. Select BreedListViewController.xib and add a new View Controller object, change its class to BreedDetailViewController, and change its nib file attribute. Connect it to the breedDetailVC IBOutlet of the File’s Owner object.

5. BreedDetailViewController.m only needs one small change. When the view will be displayed, we need to use the photo URL and actually retrieve the photo and display it in the UIImageView. iOS makes it easy to retrieve data from a URL and use it to create a UIImage (Listing 17-16).

Listing 17-16 viewWillAppear method in BreedDetailViewController.m

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.breedDescription.text = self.selectedBreed.breedDescription;
     NSURL* aURL = [NSURL URLWithString:self.selectedBreed.photoURL];
     NSData *imageData = [[NSData alloc] initWithContentsOfURL:aURL];
    UIImage *theImage = [[UIImage alloc] initWithData:imageData];
    [photo setImage:theImage];
    [theImage release];
}

6. Save all of your changes and run the application. If you create a couple of breeds with a valid photo URL, you will now be able to tap on a breed and see a description and photo.

7. Delete the DogBreeds application from the iPhone Simulator (which also deletes its Core Data database). Open and run the DogBreeds-Final project. You will find that it comes with all of the AKC groups defined and all of the dogs in the “Herding Group” complete with description and photo (Figure 17-12).

Image

Figure 17-12 Finished application with data

Distributing Core Data with Your App

By the end of this chapter you now have a fairly useful iOS application. It needs some more polish, maybe a few more fields in the Breed entity, some search functionality perhaps, but it’s well on the way to being useful. However, no one is going to buy your reference application if they have to enter all of the reference material themselves! If you have a Core Data–based application, how do you fill it with useful information and then distribute that with the application in the App Store?

The first step is to get all of your information into Core Data. With the DogBreeds application, we built all of the editing functionality into the app, so to enter the data you could just run it in the iPhone Simulator and type or paste all of the information in to create the 9 AKC groups and 168 recognized breeds. All of that information is readily available in places like Wikipedia.

Of course, using the iPhone Simulator to enter all of the information for your reference application might start to get quite tedious. If the information you want to embed is available in a standard format (e.g., a csv file), you could also add a bit of temporary code to your application that looks for the file and imports it. If you will need to maintain the information over time and release periodic updates, then the easiest thing to do might be to create a Mac OS X application for editing the data. While the topic is beyond the scope of this book, you probably noticed that iOS (Cocoa Touch) and Cocoa for Mac OS X overlap. In particular, Core Data is not specific to iOS. If you want to build a simple desktop application with a few views for entering and editing your information, you can use exactly the same xcdatamodel file. Then you’ll be able to directly use the Core Data persistent store database created by Mac OS X in your iOS application.

Once you’ve filled a Core Data persistent store database with your information, the next step is to find that database so that you can include it in your application’s Resources folder for distribution. If you used the iPhone Simulator to enter your data, the easiest way to find that database is to set a breakpoint on the following line in the persistentStoreCoordinator accessor in DogBreedsAppDelegate:

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

The storeURL variable will contain the full path to the persistent store for the iPhone Simulator on your development computer. Add that file to the Resources folder of your application so that it will be included in your application when it’s built for distribution. If you created a separate Mac OS X application for editing your data, you can use a similar trick to find the database file from that application.

With the default database embedded in the Resources folder, you can make a minor change to the persistentStoreCoordinator accessor in DogBreedsAppDelegate so that the first time your application runs, it copies the default database to the Documents directory:

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

    // Copy the default db from resources
    // if it doesn't already exist in documents
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:storePath]) {
         NSString *defaultStorePath = [[NSBundle mainBundle]
             pathForResource:@"DogBreeds" ofType:@"sqlite"];
        if (defaultStorePath) {
            [fileManager copyItemAtPath:defaultStorePath

                                 toPath:storePath error:NULL];
        }
    }

The sample DogBreeds application for this chapter included editing functionality so that we could illustrate adding, deleting, and updating objects stored using Core Data. In a typical reference application you probably wouldn’t include any editing functionality and the database could be read-only. If that is the case, then you could avoid copying the database to the Documents directory and just open it in the Resources directory (which is read-only).

If you downloaded the DogBreeds-Final project in the sample code for this chapter, you’ll see that it includes a default database with some of the dog breeds in the herding group already entered for you. We’ll start with that project in Chapter 19 and turn it into a universal application for the iPad, so having some data already present will make it much more interesting on the iPad’s large screen.

What Next?

You now have a complete and somewhat useful iOS application. In the preceding section you even learned how to distribute your application with predefined information. It should be fairly easy to repurpose the sample code from this chapter for different reference material by simply changing the data model to have the appropriate entities, attributes, and relationships and then changing the various views to display those attributes. So, what remains to be done before you can proudly submit your reference application to the App Store and start making money? There are several rough edges in our sample application that you would want to polish in order to have a quality application, but none of them are difficult.

The first thing that you might do is remove the editing functionality from the application and build a separate Mac OS X application (using the same data model) for entering and editing your reference material. If it’s reference material, you probably don’t want your customers changing it anyway. That would also free up the right button location in the navigation bar for an information icon button. At the RootViewController level that button could display a screen of general information about your application, data sources, etc. At the BreedsListViewController level it could display the description of the breed, since we’re currently only displaying the first line of it in the table view.

When tapping on the various breeds in the Herding Group in the DogBreeds-Final application, you probably noticed a delay between when you tapped and when the detailed view appeared. The viewWillAppear method loads the photo data synchronously from the Internet, which means the user waits while a potentially large photo downloads. Ideally, the application should have immediately displayed the detail view and then loaded the photo data asynchronously with some indication that the photo was loading.

Even better would be to make your reference application self-contained. When they create a new breed, you could retrieve the photo from the URL and store the actual photo data in the database. Then when the user taps on a breed you can display the detail view quickly and it will work whether the user has Internet access or not.

There are also many ways that you could make the application prettier. For instance, the UITableView that displays the list of breeds within an AKC group could use custom table cells that display a thumbnail photo of the breed alongside its name.

Summary

In this chapter, you learned the basics of Core Data framework. After learning how to model your application’s data objects, you learned how to insert, fetch, and delete instances from the data model. But you only scratched Core Data’s surface in this chapter. There are so many ways to create an NSPredicate, so many ways to create an NSFetchRequest, and so many variations on the different ways of working with the managed object context that covering them all would result in a several-hundred-page book.

To continue learning more about Core Data, refer to Apple’s documentation. Apple has heavily documented the Core Data framework. The first reference you should consult is “Apple’s Core Data Tutorial for iOS.” Consult Apple’s “Creating a Managed Object Model with Xcode” tutorial and also “Xcode Tools for Core Data” for more information on using Xcode’s data modeler. Consult Apple’s “Predicate Programming Guide” for more information on writing predicates. Finally, for a complete reference on Core Data, consult Apple’s “Core Data Programming Guide.”

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

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