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.
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:
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.
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:
We walk you through these three steps so you can add Core Data support to any existing Mac OS X application.
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:
Figure 6–2 shows the resulting screen.
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.
Once added, CoreData.framework is listed in the linked frameworks, as illustrated in Figure 6–4.
After these simple steps, your application is now ready to start tapping into the power of Core Data.
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.
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.
From the list of templates, select Data Model from the Mac OS X Core Data category, as illustrated in Figure 6–6.
Save the new file as GraphiqueModel
, as shown in Figure 6–7, and select the newly created file to see the model editor.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
#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.
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:
#import <CoreData/CoreData.h>
statement at the top.Listing 6–2 shows the resulting RecentlyUsedEquationsViewController.h
file.
#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.
#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:
NSManagedObject
and attach it to the appropriate group.The next few sections walk you through the implementation for the remember:
method.
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.
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"];
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();
}
Listing 6–4 documents the entire remember:
method. Be sure to import Equation.h
at the top of 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.
- (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];
}
}
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.
#import <Foundation/Foundation.h>
@interface EquationItem : NSObject {
@private
NSString *text;
}
@property (nonatomic, strong) NSString *text;
@end
#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.
#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
#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.
-(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.
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
[self loadChildrenForItem:(item == nil ? rootItem : item)];
return (item == nil) ? [rootItem numberOfChildren] : [item numberOfChildren];
}
- (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.
- (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.
#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.
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.
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.
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.
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.
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.
To catch the selection change, we provide an implementation of the outlineViewSelectionDidChange:
method in RecentlyUsedEquationsViewController.m
, as shown in Listing 6–15.
- (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"
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.
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.