A Core Data Tutorial Part 2: Polishing the Basics

In this installment we will add some polish to our app. This time we will spend most of our time writing code and very little in IB. We will also not be adding anything the user will really notice (unless its missing).

Things we will accomplish:

  • Make it so that new expenses will start with the current date.
  • Sort expenses by date, and make sure they re-sort as needed with edits.
  • Alphabetize the categories in both the popup menu and the list in the category tab.
  • Add the ability to copy and paste expenses.

Before we actually get started, if you have not already done so run the app, put some random data into a file and save. This will help to illustrate some of the changes we make today, and in future tutorials.

Entity Defaults

So let’s get started with something pretty simple and quick, setting new expense entities to the current date.

First the Expense entity will need a custom subclass. To start with you will have to select “MyDocument.xcdatamodel,” then select “File -> New File…” and select “Managed Object Class” as the template. If you didn’t select the data model first you will not see this option.

Image1

You can leave everything alone in the next pane (you want it to be saved to the project folder and be added to the project. In the last pane select “Expense.” Leave “Generate accessors” and “Generate Obj-C 2.0 Properties” checked and make sure “Generate validation methods” is not checked. Once you click Finish you will have two new files “Expense.h” and “Expense.m,” notice also that the model file now shows that it has been changed. If you select the model and then the Expense entity you will see that the class has been changed from “NSManagedObject” to “Expense.” Save the model file.

Image2

In “Expense.m” add the following method:

- (void) awakeFromInsert { 
	NSDate *now = [NSDate date]; 
	self.date = now; 
} 
@end 

That’s it, Save, then Build and Go to make sure everything works right and notice that now when you create a new expense it has today’s date by default. As long as we are setting up default values let’s go ahead and set the Category.name default value to “category,” the Expense.amount to “0,” and the Expense.desc to “expense.” Again remember to save, if you want Build and Go, to see the default values get dropped in for you.

Image3

So why awakeFromInsert ? awakeFromInsert is only called once in the life of an entity, this way you can do any setup needed for new entities and not worry about it being called when the model is loaded from disk.

You should also notice that none of the changes broke any old files we had around. This is important later as we will be adding a new attribute to the Expense entity in a later tutorial that will break any files that we create now if not done properly. For now you just need to know that you have to be careful when making changes to the model, the Apple documentation in Xcode covers what you can and cannot change without breaking the model.

Sorting

Now on to some sorting.

In MyDocument.h add the following outlets:

@interface MyDocument: NSPersistentDocument { 
	IBOutlet NSTableView * expenseTable ; 
	IBOutlet NSTableView * categoryTable ; 
	IBOutlet NSTableView * expenseByCatTable ; 
	IBOutlet NSArrayController *categoryPopUpController; 
} 

In MyDocument.m edit the windowControllerDidLoadNib: method to look like this:

- (void)windowControllerDidLoadNib:(NSWindowController *)windowController { 
	[super windowControllerDidLoadNib: windowController]; 
	// user interface preparation code 
	// create two sort descriptors, one for date and one for name 
	NSSortDescriptor *dateSort = [[NSSortDescriptor alloc] initWithKey: @"date" ascending: YES]; 
	NSSortDescriptor *nameSort = [[NSSortDescriptor alloc] initWithKey: @"name" ascending: YES]; 
	// Put the sort descriptors into arrays 
	NSArray *dateDescriptors = [NSArray arrayWithObject: dateSort]; 
	NSArray *nameDescriptors = [NSArray arrayWithObject: nameSort]; 
	// Now set the corrent sort descriptors for each outlet 
	// First set the tables that shows expenses to the date descriptor 
	[expenseTable setSortDescriptors: dateDescriptors]; 
	[expenseByCatTable setSortDescriptors: dateDescriptors]; 
	// Now set the descriptors for the Category table 
	[categoryTable setSortDescriptors: nameDescriptors]; 
	// For the Category popup button we have to sort the array controller not the button. 
	[categoryPopUpController setSortDescriptors: nameDescriptors]; 
} 

Next, open MyDocument.xib in IB and connect the outlets.

Image4

To make sure that everything re-sorts when things are edited turn on “Auto Rearrange Content” in each array controller:

Image6

Copy & Paste

Finally let’s make it so that the user doesn’t have to put all the same information in all the time when the same expense shows up over and over again with some Copy/Paste action. This is pulled straight from the Apple Documentation with only a few adjustments to make it work here.

To start with add the following method declarations to “Expense.h”

+ (NSArray *) keysToBeCopied; 
- (NSDictionary *) dictionaryRepresentation; 
- (NSString *) stringDescription; 

Then implement them in “Expense.m”

// Copy/Paste methods 
+ (NSArray *) keysToBeCopied { 
	static NSArray *keysToBeCopied = nil ; 
	if (keysToBeCopied == nil) { 
		// This will determine which attributes get copied. Must NOT copy relationships or it will copy the actual entity 
		// Date has been left out so that the date will default to the current date. 
		keysToBeCopied = [[NSArray alloc] initWithObjects: @"desc" , @"amount" , nil]; 
	} 
	return keysToBeCopied; 
} 
 
- (NSDictionary *) dictionaryRepresentation { 
	return [self dictionaryWithValuesForKeys: [[self class] keysToBeCopied]]; 
} 
 
- (NSString *) stringDescription { 
	// This will return the title of the category as a string 
	NSString *stringDescription = nil ; 
	NSManagedObject *category = self .category; 
	if (category != nil) { 
		stringDescription = category. name ; 
	} 
	return stringDescription; 
} 

Now add one outlet and two method declarations to “MyDocument.h.”

// Outlets for copy & paste 
IBOutlet NSArrayController * expensesArrayController; 
 
- (IBAction) copy:(id) sender; 
- (IBAction) paste:(id) sender; 
@end 

Implement those methods in “MyDocument.m.”

// For duplicating Expense entities 
- (IBAction) copy:(id) sender { 
	NSArray *selectedObjects = [expensesArrayController selectedObjects]; 
	NSUInteger count = [selectedObjects count]; 
	if (count == 0) { 
		return ; 
	} 
	NSMutableArray *copyObjectsArray = [NSMutableArray arrayWithCapacity: count]; 
	NSMutableArray *copyStringsArray = [NSMutableArray arrayWithCapacity: count]; 
 
	for (Expense *expense in selectedObjects) { 
		[copyObjectsArray addObject: [expense dictionaryRepresentation]]; 
		[copyStringsArray addObject: [expense stringDescription]]; 
	} 
 
	NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard]; 
	[generalPasteboard declareTypes: [NSArray arrayWithObjects: MSExpensesPBoardType , NSStringPboardType , nil] owner: self]; 
	NSData *copyData = [NSKeyedArchiver archivedDataWithRootObject: copyObjectsArray]; 
	[generalPasteboard setData: copyData forType: MSExpensesPBoardType]; 
	[generalPasteboard setString: [copyStringsArray componentsJoinedByString: @"\n"] forType: NSStringPboardType]; 
} 
 
- (IBAction) paste:(id) sender { 
	NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard]; 
	NSData *data = [generalPasteboard dataForType: MSExpensesPBoardType]; 
	if (data == nil) { 
		return ; 
	} 
	NSArray *expensesArray = [NSKeyedUnarchiver unarchiveObjectWithData: data]; 
	NSManagedObjectContext *moc = [self managedObjectContext]; 
	NSArray *stringArray = [[generalPasteboard stringForType: NSStringPboardType] componentsSeparatedByString: @"\n"]; 
	NSEntityDescription *cats = [NSEntityDescription entityForName: @"Category" inManagedObjectContext: moc]; 
	NSString *predString = [NSString stringWithFormat: @"%@ LIKE %%@" , @"name"]; 
	int i = 0 ; 
	for (NSDictionary *expenseDictionary in expensesArray) { 
		//create a new Expense entity 
		Expense *newExpense; 
		newExpense = (Expense *)[NSEntityDescription insertNewObjectForEntityForName: @"Expense" inManagedObjectContext: moc]; 
		// Dump the values from the dictionary into the new entity 
		[newExpense setValuesForKeysWithDictionary: expenseDictionary]; 
		// create a fetch request to get the category whose title matches the one in the array at the current index 
		NSFetchRequest *req = [[NSFetchRequest alloc] init]; 
		// set the entity 
		[req setEntity: cats]; 
		// create the predicate 
		NSPredicate *predicate = [NSPredicate predicateWithFormat: predString, [stringArray objectAtIndex: i]]; 
		// set the predicate 
		[req setPredicate: predicate]; 
		// just in case 
		NSError *error = nil ; 
		// execute the request 
		NSArray *fetchResults = [moc executeFetchRequest: req error: &error]; 
		// acquire a pointer for the correct category 
		Category *theCat = [fetchResults objectAtIndex: 0]; 
		// get the expenses set from the category 
		NSMutableSet *aSet = [theCat mutableSetValueForKey: @"expenses"]; 
		// now to add the new expense entity to the category 
		[aSet addObject: newExpense]; 
 
		i++; 
	} 
} 

You also need to add a couple of things above @implementation in “MyDocument.m.”

#import "Expense.h" 
NSString *MSExpensesPBoardType = @"MSExpensesPBoardType" ; 

Open MyDocument.xib in IB and connect the outlet we added (expensesArrayController) to the “ExpenseView Array Controller.”

Save everything, then Build & Go.

You should have just gotten an error that looks something like this:

Image5

This seems odd, especially since if you use code completion you probably got the completion for “name” and it is colored to indicate that the editor knows that it is an attribute. No matter, let’s try to fix that error so we can see copy/paste in action. Change the line with the error from dot syntax to a method call:

NSManagedObject *category = self .category; 
if (category != nil) { 
	stringDescription = [category name]; 
} 

Now Save and Build.

Well, at least it is just a warning now. It actually will work like this but we don’t like warnings so let’s see about getting rid of it.

First off, why are we getting the warning? The answer is that NSManagedObject doesn’t have a method declaration for name and really why would it, its not the Category class, its the super class.

In order to fix it we need to tell the complier that there is a class named Category and that it has a method called name. Of course we don’t have a file named “Category.h” to import so we will have to make it (seems a little silly but maybe we will need it later for something anyway).

Don’t forget that in order to get the Managed Object in the New File assistant you need to have the model selected.

This works just like last time when we made the files for “Expense.” (Hint: select the model, then New File…, Managed Object, Category, default settings).

You don’t need to add any code to either file just add #import “Category.h” to “Expense.h.”

Then we need to change the two places we refer to NSManagedObject to say Category instead:

In “Expense.h”

@property (nonatomic, retain) NSManagedObject *category; 
becomes: 
@property (nonatomic, retain) Category *category; 

Then in “Expense.m”

NSString *stringDescription = nil; 
NSManagedObject *category = self.category; 
if (category != nil) 

becomes:

NSString *stringDescription = nil; 
Category *category = self.category; 
if (category != nil) 

Now save everything then Build & Go.

That’s more like it, no errors and no warnings. Wait, you did get a warning and error free build that time right?

You should now be able to copy and paste expenses, even several at a time. The date should get set to the current date and everything else should be just like the source expense.

That’s all folks

That’s all for this time, next time we will move on to adding user preferences. If for some reason something isn’t working for you at this point here is a zip file of the project at this point, along with a sample file:

Expenses Part 2

There are two things I would like to add at this point but don’t know how to; an NSComboBox in place of the NSPopUpMenu in the Expenses tab (it would be nice to be able to type in the Category with autocompletion), the other thing is it would be nice to see how much was spent in each category per month along with the total for each month. I’m sure both of these things are fairly easy but they have eluded me thus far. For the combo box I have tried translating the bindings over but they don’t match and none of my attempts have worked. For the monthly totals I have tried numerous methods that have all failed. inserting attributes for each month with custom getter methods causes an error as soon as the second getter is fired. Attempts to use notifications have failed since they are sent far too often to be useful. I welcome any suggestions or ideas in either of these two areas and will ensure that all contributers get credit.

As always, I look forward to your comments, questions, complaints, suggestions, etc.

theMIkeSwan

Entertainment Lighting Tech, OS X & iOS developer

Leave a Reply