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.

Also if anyone knows of a better blog site to keep these tutorials please let me know, this site is annoying the crap out of me.

Advertisements

7 Responses to “A Core Data Tutorial Part 2: Polishing the Basics”

  1. Chris Hanson Says:

    The declarations of the -copy: and -paste: methods should follow the IBAction pattern:

    – (IBAction)copy:(id)sender;
    – (IBAction)paste:(id)sender;

    “id” means “any object” and so is synonymous with a pointer type already; IBAction is a #define for “void” that IB can key off of so it doesn’t assume all (void)-returning, (id)-taking methods are actions.

  2. themikeswan Says:

    Chris, You are totally right, really strange part is that I copied that from the Apple docs, you would think they would have gotten it right.
    I’ve corrected now, that’ll teach me to blindly follow Apple.

  3. mmalc Says:

    – (void)copy:sender is perfectly reasonable, particularly if you’re not connecting a user interface item directly — see for example:

    http://developer.apple.com/documentation/Cocoa/Reference/ApplicationKit/Classes/NSText_Class/Reference/Reference.html#//apple_ref/doc/uid/20000367-copy_

  4. Ian Piper Says:

    There is a minor missing step. Between these two steps:

    ‘To start with you will have to select “MyDocument.xcdatamodel,”’

    and

    ‘then select “File -> New File…”’

    you need to select the entity for which you want a subclass: in this case, Expense. If you don’t you will not see Managed Object Class as an option.

  5. themikeswan Says:

    Ian, actually as long as you have MyDocument.xcdatamodel selected in the table view above the editor and not just the source list to the left it will work, but non of the entities will be checked by default.

  6. ivucica Says:

    @Chris Hanson:
    IBAction is #define’d to void. Not specifying an argument’s data type (or a return type) defaults them to “id”.

    Hence, at compile time, (void)copy:sender is equal to (IBAction)copy:(id)sender.

    The only place “IBAction” is not the same as “void” is in Interface Builder: its parser interprets “IBAction” in a different way. This is similar to “IBOutlet”, which is #define’d to nothing

    It is also interesting that having used “IBAction” in the header, Xcode 3.2.6 will not provide autocomplete when writing implementation in case one starts writing implementation with -(IBAction). It will do so only if one starts writing it with -(void).

  7. amarsawant Says:

    Reblogged this on iOS Reblogged.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: