Chapter 6

Using Core Data

Picking up where we left off in Chapter 5, we now get a glimpse of Cocoa's powerful object storage framework, Core Data, which we use to store recently entered equations so that users can retrieve and redisplay them.

At the end of this chapter, Graphique will look like Figure 6–1. It will store the recently entered equations into Core Data storage so that the equations users enter will still be present across launches of Graphique, so that users will be able to retrieve their interesting equations even after they close and reopen Graphique.

images

Figure 6–1. Graphique with equations in Core Data

Stepping Up to Core Data

Core Data is the persistence framework that comes with Mac OS X. Core Data is not a database, and it's not a file system either. Instead, it's an abstraction layer that stands between your code and the actual storage mechanism (database, file system, and so on) you use. There are many benefits to using Core Data instead of directly welding your code onto your chosen storage mechanism. These are some of them:

  • Unified API for storing, retrieving, and searching
  • Ability to change storage without changing much of your code
  • Automatic bidirectional object to storage mapping
  • Ability to use the built-in Xcode tools

In this section, we first go through the steps required to add Core Data to the existing Graphique project. We then create a data model for storing recently used equations. We then hook all this up into the user interface to tie everything together.

Adding Core Data to the Graphique Application

If you've done enough planning and you already know you will use Core Data for your application, you can attach it to your application directly from the New Project wizard when you go through it. In many cases, however, you won't realize until later that you want to use Core Data for your project. In this section, we show you how to add Core Data to an existing project.

Enabling an application to leverage Core Data is a three-step process:

  1. Add the Core Data framework.
  2. Create a data model.
  3. Initialize the managed object context.

We walk you through these three steps so you can add Core Data support to any existing Mac OS X application.

Adding the Core Data Framework

In the Objective-C world, libraries are bundled into frameworks, and an application refers to a framework to gain access to those libraries. To see which frameworks are linked to your application, navigate to the project's build phases with the following steps:

  1. Select the Graphique project at the top of the Project navigator.
  2. Once the project information is displayed, select the Graphique target.
  3. Click the Build Phases tab.
  4. Expand the item called Link Binary With Libraries.

Figure 6–2 shows the resulting screen.

images

Figure 6–2. The frameworks linked to the application

The application should be linked to the Cocoa framework, which is the library that we've been using all along and that provides all the windows, buttons, and so on.

Click the + button right below the linked frameworks to add a new framework. When the framework browser opens, look for CoreData.framework and add it to the application, as shown in Figure 6–3.

images

Figure 6–3. Adding the Core Data framework to the application

Once added, CoreData.framework is listed in the linked frameworks, as illustrated in Figure 6–4.

images

Figure 6–4. The Core Data framework added to the application

After these simple steps, your application is now ready to start tapping into the power of Core Data.

Creating a Data Model

Core Data is a persistence framework. It is a unified bridge between your objects (including the relationships among them) and a data persistence mechanism such as a database or an XML file, for example. When using Core Data, you rarely have to worry about the actual persistence mechanism. Everything rotates around an object model that defines how your data is laid out. Core Data converts that model into a data model if your backend is a database or an XML schema if your backend is an XML file. Regardless of the actual storage mechanism we use, we have to create an object model.

NOTE: Note that we use the term object model and not data model. Data model is a term commonly used in the relational database world to represent the physical layout of the data in a database. This task is left to Core Data. Instead, we define an object model, which serves to define how we want our data objects to relate to one another.

For the Graphique application, we want to use Core Data to store the recently used equations. To keep everything neatly organized, we want to also create groups of equations. Our object model should therefore contain two kinds of objects: Equations and Groups.

Designing the Data Model

Since we added Core Data after the project was already created, we need to create the new object model. Right-click Supporting Files in the Project navigator and select New File, as shown in Figure 6–5.

images

Figure 6–5. Adding a new object model file

From the list of templates, select Data Model from the Mac OS X Core Data category, as illustrated in Figure 6–6.

images

Figure 6–6. Adding a new data model

Save the new file as GraphiqueModel, as shown in Figure 6–7, and select the newly created file to see the model editor.

images

Figure 6–7. Saving the new data model

The most important feature of a data model is the list of entities. Entities represent data objects, in other words, the objects that that contain the data and that we want to persist in the data store. Since we just created the model, there is no entity in it. Click the Add Entity button in the model editor and name the new entity Equation, as illustrated in Figure 6–8.

images

Figure 6–8. Adding a new entity

The list of entities should now contain only an entry for Equation. Much like objects, entities have attributes. Attributes contain the data and can use one of several predefined data types. Table 6-1 lists all the Core Data data types and what Objective-C type they map to.

images

images

NOTE: Transformable attributes are a way to tell Core Data that you would like to use a nonsupported data type in your managed object and that you will help Core Data by providing code to transform the attribute data at persist time into a supported type. We don't expand on this more advanced subject in this book.

A recently used equation is defined only by its string representation and a timestamp, so the equation entity will have two attributes: a representation attribute of type String and a timestamp attribute of type Date.

In the Core Data model editor, make sure the Equation entity is selected, and in the Attributes section, click the + button to add a new attribute. Call the attribute representation and set its type to String. Repeat the procedure to add the timestamp attribute, using the Date type this time. The Equation entity should be defined as illustrated in Figure 6–9.

images

Figure 6–9. The Equation entity

Since we want to use arbitrary groups to create groups of equations, we have to define the Group entity in the same fashion. We define our groups only by a name. In a more evolved application, you may want to include other attributes such as creation time or a description. For now, and to keep things simpler, a group has only a name.

Create the Group entity and add a name attribute of type String, as shown in Figure 6–10.

images

Figure 6–10. The Group entity

Near the bottom right of the Core Data model editor, there is an Editor Style button. Use it to toggle from Table view to Graph view. The graph illustrates the relationships between entities. As you can see in Figure 6–11, no relationship currently exists between groups and equations.

images

Figure 6–11. Core Data entities in the graph view

Since groups contain equations (or equations belong to groups), we need to create the relationship between the two. Switch the editor back to table style. Select the Equation entity and click the + button in the Relationships section. Name the new relationship group and set the destination to Group. This will tell Core Data that an equation might belong to a group. To make sure that Core Data enforces that all equations belong to a group, you must expand the Utilities panel, make sure the newly created relationship is selected, and uncheck the Optional check box, as shown in Figure 6–12.

images

Figure 6–12. Relationship from Equation to Group

To help Core Data manage the object graph, Apple strongly recommends that each relationship have an inverse relationship. For each relationship from entity A to entity B, we must create an inverse relationship from B to A. Select the Group entity and add a new relationship called equations, set its destination to Equation, and its inverse to group. Since a group may contain more than one equation, select To-Many Relationship in the Utilities panel. This tells Core Data that this relationship may lead to multiple equation entities. Since a group may be empty (in other words, does not contain any equations), we leave this relationship as optional. Figure 6–13 shows the resulting relationship configuration.

images

Figure 6–13. The inverse relationship from Group to Equation

This completes our data model for now. If you switch the editor to graph view, as shown in Figure 6–14, you can see that now the two entities are related. Note how the relationship from Group to Equation has a double arrowhead. This is to symbolize the To-many nature of the relationship.

images

Figure 6–14. The data model in graph view

Initialize the Managed Object Context

Now back to our code. So far, we've made our project aware of Core Data by linking the Core Data framework, and we've defined our data model by creating entities and relationships. When data are pulled out of the persistent store, they are materialized as subclasses of NSManagedObject and offer methods to access their data. Any application using Core Data, however, must first initialize the framework before any operation can be performed. The framework uses the NSManagedObjectContext class as an interface between your code and the persistent store. The initialization process consists of properly setting up the managed object context and the other classes it relies upon, as shown in Figure 6–15.

images

Figure 6–15. The high-level Core Data framework layout

The managed object context relies on a persistent store coordinator (NSPersistentStoreCoordinator) that serves to reconcile the physical persistent store (database, XML file, and so on) with the object model we created and that gets loaded into the framework as an NSManagedObjectModel instance.

The initialization process consists of setting up the NSManagedObjectModel, the NSPersistentStoreCoordinator, and finally the NSManagedObjectContext. For the most part, these steps are identical from project to project, and we give them to you in the following text. Open GraphiqueAppDelegate.h and add an import statement near the top:

#import <CoreData/CoreData.h>

Define three properties for the three main Core Data components, as illustrated in Listing 6–1: managedObjectContext, managedObjectModel, and persistentStoreCoordinator.

Listing 6–1. GraphiqueAppDelegate.h with Core Data

#import <Cocoa/Cocoa.h>
#import <CoreData/CoreData.h>

@class EquationEntryViewController;
@class GraphTableViewController;
@class RecentlyUsedEquationsViewController;
@class PreferencesController;

@interface GraphiqueAppDelegate : NSObject <NSApplicationDelegate> {
  @private
  NSManagedObjectContext *managedObjectContext_;
  NSManagedObjectModel *managedObjectModel_;
  NSPersistentStoreCoordinator *persistentStoreCoordinator_;
}

@property (strong) IBOutlet NSWindow *window;
@property (weak) IBOutlet NSSplitView *horizontalSplitView;
@property (weak) IBOutlet NSSplitView *verticalSplitView;
@property (strong) EquationEntryViewController *equationEntryViewController;
@property (strong) GraphTableViewController *graphTableViewController;
@property (strong) RecentlyUsedEquationsViewController *recentlyUsedEquationsViewController;
@property (strong) PreferencesController *preferencesController;

@property (nonatomic,readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic,readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic,readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void)changeGraphLineColor:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;

@end

Open GraphiqueAppDelegate.m to implement the getter methods for these properties. We start with managedObjectModel :

- (NSManagedObjectModel *)managedObjectModel {    
  if (managedObjectModel_ != nil) {
    return managedObjectModel_;
  }
  managedObjectModel_ = [NSManagedObjectModel mergedModelFromBundles:nil];
  return managedObjectModel_;
}

Note that we don't specify the model we created specifically; mergedModelFromBundles: will find and load all appropriate model files in the project. Now that we've loaded the object model, we can leverage it in order to create the persistent store handler. This example uses NSSQLiteStoreType in order to indicate that the storage mechanism should rely on a SQLite database, as shown here:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {    
  if (persistentStoreCoordinator_ != nil) {
    return persistentStoreCoordinator_;
  }

  NSString* dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
  NSURL *storeURL = [NSURL fileURLWithPath: [dir stringByAppendingPathComponent: @"Graphique.sqlite"]];

  NSError *error = nil;
  persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
  if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }    

  return persistentStoreCoordinator_;
}

NOTE: We are telling Core Data to use a SQLite database called Graphique.sqlite and store it in the user's Documents folder. You may change this to store the file anywhere you'd like.

Finally, we initialize the context from the persistent store that we just defined:

- (NSManagedObjectContext *)managedObjectContext {    
  if (managedObjectContext_ != nil) {
    return managedObjectContext_;
  }

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

The application can now use the managed object context to store and retrieve entities.

Storing Recently Used Equations

In the Graphique application, the RecentlyUsedEquationsViewController class is the user interface controller responsible for dealing with recently used equations. Since it will need to interact with the persisted data, the first step is to give it access to the NSManagedObjectContext object that represents the interface to the persistent store. Open RecentlyUsedEquationsViewController.h and add the following:

  1. An #import <CoreData/CoreData.h> statement at the top.
  2. A new property for holding the managed object context.

Listing 6–2 shows the resulting RecentlyUsedEquationsViewController.h file.

Listing 6–2. RecentlyUsedEquationsViewController.h

#import <Cocoa/Cocoa.h>
#import <CoreData/CoreData.h>

#import "GroupItem.h"

@interface RecentlyUsedEquationsViewController : NSViewController <NSOutlineViewDataSource, NSSplitViewDelegate> {
@private
  GroupItem *rootItem;    
  NSManagedObjectContext *managedObjectContext;
}
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

@end

Don't forget to open RecentlyUsedEquationsViewController.m and add a @synthesize statement for the new property right after the @implementation statement:

@synthesize managedObjectContext;

Finally, we have to make sure the app delegate passes the context along when it initializes the controller. Open GraphiqueAppDelegate.m and edit the applicationDidFinishLaunching: method to match this code:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
  equationEntryViewController = [[EquationEntryViewController alloc] initWithNibName:@"EquationEntryViewController" bundle:nil];
  [self.verticalSplitView replaceSubview:[[self.verticalSplitView subviews] objectAtIndex:1] with:equationEntryViewController.view];

  graphTableViewController = [[GraphTableViewController alloc] initWithNibName:@"GraphTableViewController" bundle:nil];
  [self.horizontalSplitView replaceSubview:[[self.horizontalSplitView subviews] objectAtIndex:1] with:[graphTableViewController view]];

  recentlyUsedEquationsViewController = [[RecentlyUsedEquationsViewController alloc]
initWithNibName:@"RecentlyUsedEquationsViewController" bundle:nil];
  recentlyUsedEquationsViewController.managedObjectContext = self.managedObjectContext;
  [self.verticalSplitView replaceSubview:[[self.verticalSplitView subviews] objectAtIndex:0] with:[recentlyUsedEquationsViewController view]];
  self.verticalSplitView.delegate = recentlyUsedEquationsViewController;

  [[NSColorPanel sharedColorPanel] setTarget:self];
  [[NSColorPanel sharedColorPanel] setAction:@selector(changeGraphLineColor:)];
}

Now, let's go back to RecentlyUsedEquationsViewController.h and define a new method that other controllers can use to tell it to remember an equation:

-(void)remember:(Equation*)equation;

With the new method, RecentlyUsedEquationsViewController.h should look like Listing 6–3.

Listing 6–3. RecentlyUsedEquationsViewController.h with a New Method for Remembering Equations

#import <Cocoa/Cocoa.h>
#import <CoreData/CoreData.h>

@class GroupItem;
@class Equation;

@interface RecentlyUsedEquationsViewController : NSViewController
<NSOutlineViewDataSource, NSSplitViewDelegate> {
@private
  GroupItem *rootItem;    
  NSManagedObjectContext *managedObjectContext;
}
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;

- (void)remember:(Equation*)equation;

@end

NOTE: Do not confuse the Equation object with the Equation entity we created earlier in the model. Right now, while they represent the same thing semantically, they aren't related in code. Right now, data in the Equation entity is represented as an object of type NSManagedObject and not an object of type Equation. It is possible to make Core Data use a custom class (in other words, other than NSManagedObject) to represent data entities as objects, but it falls out of the scope of this book and encourage you to get a book dedicated to Core Data for more information.

Open RecentlyUsedEquationsViewController.m to implement the remember: method. The algorithm for the method is as follows:

  1. Create a group name based on the current date (month/day/year).
  2. Query Core Data to find out whether the group already exists.
  3. If it does exist, then use it; otherwise, create a new one, save it, and use it.
  4. Create a new equation entity as an NSManagedObject and attach it to the appropriate group.
  5. Commit to the persistent store.

The next few sections walk you through the implementation for the remember: method.

Querying the Persistent Store to Get the Group Entity

Creating the group name is trivial using the NSDateFormatter object:

NSDate *today = [NSDate date];
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"EEEE MMMM d, YYYY"];
NSString *groupName = [dateFormat stringFromDate:today];

We then query the persistent store to see whether the group exists already. Retrieving objects from the persistent store is done using the NSFetchRequest object, which defines what to retrieve:

// Create the fetch request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Define the kind of entity to look for
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Group" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Add a predicate to further specify what we are looking for
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name=%@", groupName];
[fetchRequest setPredicate:predicate];

NSArray *groups = [self.managedObjectContext executeFetchRequest:fetchRequest
error:nil];
NSManagedObject *groupMO = nil;

if(groups.count > 0) {
  // We found one, use it
  groupMO = [groups objectAtIndex:0];
}
else {
  // We need to create a new group because it did not exist
  groupMO = [NSEntityDescription insertNewObjectForEntityForName:@"Group" inManagedObjectContext:self.managedObjectContext];

  // set the name
  [groupMO setValue:groupName forKey:@"name"];  
}

NOTE: We use the key/value pair generic accessor methods to interact with the data contained in the NSManagedObject. [object valueForKey:@"myAttribute"] will retrieve the value of myAttribute, while [object setValue:@"theValue" forKey:@"myAttribute"] will set the value of myAttribute.

At this point, we have a managed object representing the right group. If the group did not exist before, we added it using the insertNewObjectForEntityForName:inManagedObjectContext: method.

Creating the Equation Managed Object and Adding It to the Persistent Store

Now that we have a valid group managed object, we simply create a new equation managed object and link it to the group:

NSManagedObject *equationMO = [NSEntityDescription insertNewObjectForEntityForName:@"Equation" inManagedObjectContext:self.managedObjectContext];

// set the timestamp and the representation
[equationMO setValue:equation.text forKey:@"representation"];
[equationMO setValue:[NSDate date] forKey:@"timestamp"];
[equationMO setValue:groupMO forKey:@"group"];

Committing

In Core Data, nothing is permanent until you tell it to commit. Upon committing, the managed object context will flush all of its content to the persistent store, making it permanent. The commit operation is triggered by calling the save: method:

NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
  NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
  abort();
}

Putting Everything Together into the Final Method

Listing 6–4 documents the entire remember: method. Be sure to import Equation.h at the top of RecentlyUsedEquationsViewController.m.

Listing 6–4. The remember: Method in RecentlyUsedEquationsViewController.m

-(void)remember:(Equation*)equation
{
  NSDate *today = [NSDate date];
  NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
  [dateFormat setDateFormat:@"EEEE MMMM d, YYYY"];
  NSString *groupName = [dateFormat stringFromDate:today];

  // Create the fetch request
  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
  // Define the kind of entity to look for
  NSEntityDescription *entity = [NSEntityDescription entityForName:@"Group" inManagedObjectContext:self.managedObjectContext];
  [fetchRequest setEntity:entity];
  // Add a predicate to further specify what we are looking for
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name=%@", groupName];
  [fetchRequest setPredicate:predicate];

  NSArray *groups = [self.managedObjectContext executeFetchRequest:fetchRequest
error:nil];
  NSManagedObject *groupMO = nil;

  if(groups.count > 0) {
    // We found one, use it
    groupMO = [groups objectAtIndex:0];
  }
  else {
    // We need to create a new group because it did not exist
    groupMO = [NSEntityDescription insertNewObjectForEntityForName:@"Group" inManagedObjectContext:self.managedObjectContext];

    // set the name
    [groupMO setValue:groupName forKey:@"name"];  
  }

  NSManagedObject *equationMO = [NSEntityDescription insertNewObjectForEntityForName:@"Equation" inManagedObjectContext:self.managedObjectContext];

  // set the timestamp and the representation
  [equationMO setValue:equation.text forKey:@"representation"];
  [equationMO setValue:[NSDate date] forKey:@"timestamp"];
  [equationMO setValue:groupMO forKey:@"group"];

  NSError *error = nil;
  if (![self.managedObjectContext save:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }
}

Finally, we must call this method. We want to remember an equation when it is entered. Open EquationEntryViewController.m and add an import statement at the top:

#import "RecentlyUsedEquationsViewController.h"

Then, edit the equationEntered: method to match Listing 6–5.

Listing 6–5. The updated equationEntered: Method

- (IBAction)equationEntered:(id)sender
{
  NSLog(@"Equation entered");  
  GraphiqueAppDelegate *delegate = [NSApplication sharedApplication].delegate;

  Equation *equation = [[Equation alloc] initWithString: [self.textField stringValue]];

  NSError *error = nil;
  if(![equation validate:&error])
  {
    // Validation failed, display the error
    NSAlert *alert = [[[NSAlert alloc] init] autorelease];
    [alert addButtonWithTitle:@"OK"];
    [alert setMessageText:@"Something went wrong. "];
    [alert setInformativeText:[NSString stringWithFormat:@"Error %d: %@", [error
code],[error localizedDescription]]];
    [alert setAlertStyle:NSInformationalAlertStyle];

    [alert beginSheetModalForWindow:delegate.window modalDelegate:self didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
  }
  else
  {
    [delegate.recentlyUsedEquationsViewController remember: equation];
    [delegate.graphTableViewController draw: equation];
  }
}

Reloading Recently Used Equations

We have everything in place to store equations as they are entered. Of course, we haven't wired the outline view with the data, so the application won't appear to be doing anything different. You can launch the application nonetheless to check that everything is hooked up correctly.

Once the application is started, try typing an equation like x2 (in other words, “x squared”) for example and graph it. Everything should behave like before. But open the Terminal.app application and go into the Documents folder to open the data store:

sqlite3 ~/Documents/Graphique.sqlite
SQLite version 3.7.5
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .schema
CREATE TABLE ZEQUATION ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZGROUP INTEGER, ZTIMESTAMP TIMESTAMP, ZREPRESENTATION VARCHAR );
CREATE TABLE ZGROUP ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZNAME VARCHAR );
CREATE TABLE Z_METADATA (Z_VERSION INTEGER PRIMARY KEY, Z_UUID VARCHAR(255), Z_PLIST
BLOB);
CREATE TABLE Z_PRIMARYKEY (Z_ENT INTEGER PRIMARY KEY, Z_NAME VARCHAR, Z_SUPER INTEGER, Z_MAX INTEGER);
CREATE INDEX ZEQUATION_ZGROUP_INDEX ON ZEQUATION (ZGROUP);
sqlite> select * from ZGROUP;
1|2|1|Wednesday July 6, 2011
sqlite> select * from ZEQUATION;
1|1|1|1|331670216.49823|x2

Use the .schema command to display the schema. You can see that Core Data created two tables to hold the equations and group. By querying each table, you can see that a new group was created and that it contains the equation you just typed. The data is getting stored, and now we just have to pull it back out and display it properly.

Before we hook everything to the user interface, we need to edit the GroupItem and EquationItem objects. These objects are used as the data structure of the outline view we want to populate. First, we edit EquationItem to make text a property instead of a method that returns dummy data. Edit EquationItem.h and EquationItem.m to look like Listing 6–6 and Listing 6–7, respectively.

Listing 6–6. EquationItem.h

#import <Foundation/Foundation.h>

@interface EquationItem : NSObject {
@private
  NSString *text;
}
@property (nonatomic, strong) NSString *text;

@end

Listing 6–7. EquationItem.m

#import "EquationItem.h"

@implementation EquationItem

@synthesize text;

- (NSInteger)numberOfChildren {
  return 0;
}

@end

Next we want to make sure we can reload the data in a group item without having to throw away the object each time, so we add a reset: method that will remove its children and a boolean flag to help us keep track of whether the members of the group have been loaded. Edit GroupItem.h and GroupItem.m to match Listing 6–8 and Listing 6–9, respectively.

Listing 6–8. GroupItem.h

#import <Foundation/Foundation.h>

@interface GroupItem : NSObject {
@private
  NSString *name;
  NSMutableArray *children;
  BOOL loaded;
}

@property (nonatomic, strong) NSString *name;
@property BOOL loaded;

- (NSInteger)numberOfChildren;
- (id)childAtIndex:(NSUInteger)n;
- (NSString*)text;

- (void)addChild:(id)childNode;
- (void)reset;

@end

Listing 6–9. GroupItem.m

#import "GroupItem.h"

@implementation GroupItem

@synthesize name, loaded;

- (id)init
{
  self = [super init];
  if (self) {
    children = [[NSMutableArray alloc] init];
    loaded = NO;
  }
  return self;
}

- (void)addChild:(id)childNode
{
  [children addObject:childNode];
}

- (NSInteger)numberOfChildren
{
  return children.count;
}

- (id)childAtIndex:(NSUInteger)n
{
  return [children objectAtIndex:n];
}

- (void)reset
{
  [children removeAllObjects];
  loaded = NO;
}

- (NSString*)text
{
  return name;
}

- (void)dealloc
{
  [children release];
  [super dealloc];
}

@end

We now turn our interest toward the RecentlyUsedEquationsViewController class. To help read the data from the persistent store, we add a method that will take an item, either a GroupItem or an EquationItem, and populate its children if it's a group. If the item is an equation, nothing is done since equations are leaves in our outline tree. Open RecentlyUsedEquationsViewController.h and declare the following method:

-(void)loadChildrenForItem:(id)item;

Then edit RecentlyUsedEquationsViewController.m to provide the method implementation, as shown in Listing 6–10.

Listing 6–10. The loadChildrenForItem: Method

-(void)loadChildrenForItem:(id)item
{
  // If the item isn't a group, there's nothing to load
  if(![item isKindOfClass:GroupItem.class]) return;
  GroupItem *group = (GroupItem*)item;

  // No point reloading if it's already been loaded
  if(group.loaded) return;

  // Wipe out the nodes children since we're about to reload them
  [group reset];

  // If the group is the rootItem, then we need to load all the available groups. If
not, then we only load the
  // equations for that group based on its name

  if(group == rootItem) {
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Group" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

    NSArray *groups = [self.managedObjectContext executeFetchRequest:fetchRequest
error:nil];
    for(NSManagedObject *obj in groups) {
      GroupItem *groupItem = [[GroupItem alloc] init];
      groupItem.name = [obj valueForKey:@"name"];
      [group addChild:groupItem];
    }

  }
  else {
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Equation" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    // Add a predicate to further specify what we are looking for
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"group.name=%@",
group.name];
    [fetchRequest setPredicate:predicate];

    NSArray *equations = [self.managedObjectContext executeFetchRequest:fetchRequest
error:nil];

    for(NSManagedObject *obj in equations) {
      EquationItem *equationItem = [[EquationItem alloc] init];
      equationItem.text = [obj valueForKey:@"representation"];
      [group addChild:equationItem];
    }

  }

  // Mark the group as properly loaded
  group.loaded = YES;
}

The previous method loads only the data for a given node. It does not recurse down the children to load their data as well. This is commonly referred to as lazy loading. Lazy loading is a technique that consists in loading only the data necessary to display the user interface. We make sure to call the previous method only when the user interface needs the information. The information is needed when the outline view wants to determine whether a node can be expanded (in other words, a group with equations) or to find out how many children a node has (in other words, how many equations in a given group). While still in RecentlyUsedEquationsViewController.m, edit the outlineView:numberOfChildrenOfItem: and outlineView:isItemExpandable: methods to match Listing 6–11 and Listing 6–12.

Listing 6–11. Loading a Group When the Number of Children Is Needed

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
  [self loadChildrenForItem:(item == nil ? rootItem : item)];
  return (item == nil) ? [rootItem numberOfChildren] : [item numberOfChildren];
}

Listing 6–12. Loading a Group to Find Out If the Group Is Expandable

- (BOOL)outlineView:(NSOutlineView *)_outlineView isItemExpandable:(id)item
{
  return [self outlineView:_outlineView numberOfChildrenOfItem:item] > 0;
}

Of course, we need to make sure we remove any dummy data we had created in a prior chapter. Clean up initWithNibName:bundle: to make it look like Listing 6–13.

Listing 6–13. The initWithNibName:bundle: Method

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  if (self) {
    rootItem = [[GroupItem alloc] init];
  }
  return self;
}

If you launch the application now, the outline view should load the equation you had recorded in the previous run, but if you graph another equation, despite that it is recorded, the table does not refresh itself. To address this issue, we need to make the controller aware of the outline view so it can tell it to refresh. To do this, we add a new property of type NSOutlineView and tag it with IBOutlet so Interface Builder can see it. RecentlyUsedEquationsViewController.h should match Listing 6–14.

Listing 6–14. RecentlyUsedEquationsViewController.h

#import <Cocoa/Cocoa.h>
#import <CoreData/CoreData.h>

#import "Equation.h"
#import "GroupItem.h"

@interface RecentlyUsedEquationsViewController : NSViewController <NSOutlineViewDataSource, NSSplitViewDelegate>
{
@private
  GroupItem *rootItem;    
  NSManagedObjectContext *managedObjectContext;
  NSOutlineView *outlineView;
}
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong) IBOutlet NSOutlineView *outlineView;

-(void)remember:(Equation*)equation;
-(void)loadChildrenForItem:(id)item;

@end

Make sure you open RecentlyUsedEquationsViewController.m to add the @synthesize directive after @implementation :

@synthesize outlineView;

Finally, open RecentlyUsedEquationsViewController.xib and link the new outlineView property from the File's Owner to the Outline View object. Select the File's Owner, open the Connections inspector, and drag the circle next to the outlineView outlet to the Outline View object. The result should look like Figure 6–16.

images

Figure 6–16. The outlineView outlet linked to the user interface

All we have to do now is tell the outline view to reload itself when we are told to remember an equation. In RecentlyUsedEquationsViewController.m, add the following couple of lines of code at the end of the remember: method:

// Reload outline
[rootItem reset];
[outlineView reloadData];

Launch the app and see how it now remembers equations as you graph them.

Tightening the Control over the Outline View

So far, we have loaded our data into the outline view. Now we need to deal with the user interacting with the view. Much like most other Cocoa components, outline views use a delegate to handle interactions. We now set our controller as the delegate for the outline view and implement the necessary methods to catch the events we care about.

Using NSOutlineViewDelegate

If you've followed this book page by page and notice the naming patterns, you most likely already guessed that the outline view delegates must conform to the NSOutlineViewDelegate protocol. This protocol has no required methods. Edit the interface definition of RecentlyUsedEquationsViewController.h to add NSOutlineViewDelegate in the list of protocols it conforms to:

@interface RecentlyUsedEquationsViewController : NSViewController
<NSOutlineViewDataSource, NSSplitViewDelegate, NSOutlineViewDelegate>

All that is left to do is to edit RecentlyUsedEquationsViewController.xib, select the Outline View object, and connect its delegate to the controller (that is, the File's Owner). The resulting connection should look like Figure 6–17.

images

Figure 6–17. The NSOutlineView delegate properly connected

At this point, the outline view is told to send all events to its delegate, the RecentlyUsedEquationsViewController controller. We can now intercept the events we care about and do what we need to do.

Handling Equations Selection

The most obvious use of the recently used equations is to retrace them by selecting them from the outline view. In addition to the methods it declares, the delegate protocol is automatically registered to receive messages corresponding to NSOutlineView notifications. These inform the delegate when the selection changes or is about to change, when a column is moved or resized, and when an item is expanded or collapsed. See Table 5-2 for the delegate messages and the corresponding notification types.

images

To catch the selection change, we provide an implementation of the outlineViewSelectionDidChange: method in RecentlyUsedEquationsViewController.m, as shown in Listing 6–15.

Listing 6–15. Handling Selection Change in an Outline View

- (void)outlineViewSelectionDidChange:(NSNotification *)notification
{
  NSOutlineView *outlineView_ = [notification object];
  NSInteger row = [outlineView_ selectedRow];

  id item = [outlineView_ itemAtRow:row];

  // If an equation was selected, deal with it
  if([item isKindOfClass:EquationItem.class]) {
    EquationItem *equationItem = item;

    Equation *equation = [[Equation alloc] initWithString:equationItem.text];

    GraphiqueAppDelegate *delegate = [NSApplication sharedApplication].delegate;

    [delegate.equationEntryViewController.textField setStringValue: equation.text];
    [delegate.graphTableViewController draw:equation];    

    [delegate.equationEntryViewController controlTextDidChange: nil];
  }
}

Make sure you add the proper import statements at the top of RecentlyUsedEquationsViewController.m :

#import "GraphiqueAppDelegate.h"
#import "EquationEntryViewController.h"
#import "GraphTableViewController.h"

Preventing Double-Clicks from Editing

Our outline view is populated from the database, so we don't want users to be able to edit its nodes by double-clicking them. An outline view always asks its delegate before allowing a cell to be edited. All we have to do is make sure we always say no. Add an implementation for the appropriate method to RecentlyUsedEquationsViewController.m, as shown here:

- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn
*)tableColumn item:(id)item
{
  return NO;
}

Launch the app and try selecting an equation from the recently used equations. It populates the entry field and draws the equation as expected.

Summary

In this chapter, you learned about Cocoa's powerful object storage mechanism, Core Data, and got a taste for how to use it. Feel free to experiment with Core Data and read further on the topic to add power to your applications. Don't be fooled by the simplicity of the API; Core Data has much more to offer and should be on your mind anytime you think about storing structured application data. We strongly encourage you to pick up a book dedicated to the subject.

In the next chapter, we integrate Graphique more fully with the Mac OS X Lion desktop so that it takes advantage of what the Lion environment offers.

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

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