Updated version of Expenses Part 1

August 2, 2011

I have posted the update version to the first part of my tutorial on Core Data. This version of the tutorial covers Xcode 4, mostly covering the bindings portion since the prototyping tool didn’t make it into Xcode 4. You can read the post here. As a reminder I will not be adding any tutorials to this site anymore as I have moved my blog to my main development website theMikeSwan.com.

Blog moving

July 20, 2011

This is just a quick note to let you all know I have moved my blog to one of my domains. It is now at http://www.theMikeSwan.com/blog. Now that Lion has been officially released I plan to update the tutorials for Lion & Xcode 4.1 (I should have some time next week). All of these updates will appear only on my new blog! I will add a note to each of the posts here as they get updated on the new site for the sake of those that find them through Google.

Expenses Part 5: Printing Core Data

August 5, 2010

Update 21 Dec 2011: I have added the classes mentioned here to an open source repo on GitHub. You can find it at https://github.com/theMikeSwan/TMSSupportClasses. There is also a class to help with implementing iCloud for Mac OS X. Over time I will be adding more support classes and I will add blog posts for them at http://www.theMikeSwan.com/blog.

At some point along the line there is still a good chance the user is going to want to print data from our application, either to paper or to a PDF document. So, today we will be talking about just that, printing.
As before here is a link to the project as we left it at the end of our last edition.

NSTableView and Printing

You can, if you want just print the NSTableView that has all the expenses in it and call it done. However, as you can see below things may not work how you expect them to.
The first image is of the top of the first page. You may have noticed that there are no headers for the table, in our case it doesn’t really affect us that much since each column is pretty obvious. The first row is also a different color from any of the other rows, because it is selected still. Also, the arrows are still visible on the last row for the popup menus.
In the second image you can see that the page break is in the middle of a row, this is something we really need to find a way around.
All of these issues stem from the fact that NSTableView isn’t really meant to be printed to paper, it is designed to be drawn on screen only. We could dive into the inner workings of how NSTableView draws itself and make a custom subclass that would look at the drawing context to see if we were drawing to screen or page to work around these problems. We could also follow the common suggestion of using WebKit to basically make a webpage that we print. WebKit does has the advantage of being able to use CSS to style the print (you could even let the user make custom CSS themes). You do of course have to drag WebKit into the mix and write HTML on the fly to dump into a web view, but it’s not really a bad option if you want to go that way.
The third option, and the one we will be using today, is to use the wonderful NSTextTable class. NSTextTable has been around since Mac OS X 10.4 so anyone that is actually going to buy your application should meet the minimum system requirements.

What is NSTextTable

NSTablePrint is a table made of text, much like tables in HTML. You create a table and set some basic info like how many columns it will have, if borders collapse, etc. Then you build an NSTextTableBlock for each cell in the table adding it to the correct row and column number. You can even tell a block to span multiple rows or columns if you want. The best part is that this table just goes into a regular NSTextView if you want it on screen (it can even be part of a much larger layout with regular text paragraphs and the like). The most tedious part of using NSTextTable is building all of those cells…

MSTablePrint

Since building NSTextTableBlocks is the most tedious part of using NSTextTable I abstracted it out into it’s own class, MSTablePrint. Because I love all of you so much I’m giving that class to you as open source! You do still have to do a little work to use this class as it needs an array of arrays of strings (don’t worry if that doesn’t really make sense yet, it will). MSTablePrint will take that array and return an NSAttributedString with an NSTextTable in it filled with your data. MSTablePrint also allows you to do things like specify a header row and/or column (with their own color, font, etc), specify the general cell format (color, font, etc), and if you want to use alternating rows. You can check out the read me file included with the class for more information. Since we will be using MSTablePrint for this project you should download it now:
Once you have the zip file downloaded open it up and drag the .h and .m file to your project to add them.

Getting Ink on the Page

Now that we have MSTablePrint added to our project we should use it to actually print something out. The first thing we need to do is add a method declaration to MyDocument.h :
- (NSArray *)rowArrayForItem:(Expense *)item;
Next we need to import MSTablePrint in MyDocument.m. After that we need to implement both printOperationWithSettings: error: and rowArrayForItem:. We will start with rowArrayForItem: that will give us an array of strings formatted the way we want them to look when printed:
 (NSArray *)rowArrayForItem:(Expense *)item {
    // This method is only available in 10.6 and later for 10.5 follow the commented code below.
    NSString *dateString = [NSDateFormatter localizedStringFromDate:item.date dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterNoStyle];
    /*
        NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
        [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
        [dateFormatter setDateStyle:dateStyle];
        [dateFormatter setTimeStyle:timeStyle];
        NSString *dateString = [dateFormatter stringForObjectValue:item.date];
    */
    // This method is also only available in 10.6 and later, for 10.5 refer to the code above for the date formatter making subistutions as needed.
    NSString *amountString = [NSNumberFormatter localizedStringFromNumber:item.amount numberStyle:NSNumberFormatterCurrencyStyle];
    NSString *descriptionString = item.desc;
    NSString *categoryString = item.category.name;
    return [NSArray arrayWithObjects:dateString, amountString, descriptionString, categoryString, nil]; // The order here will match the order in the text table
}
Now for printOperationWithSettings: error: this is called automatically to the front document by the system when the user selects Print… from the File menu:
- (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)printSettings error:(NSError **)outError {
    NSPrintInfo *pInfo = [self printInfo];
    [pInfo setHorizontalPagination:NSFitPagination]; // This will keep the text table from spanning more than one page across.
    [pInfo setVerticallyCentered:NO]; // Keep the table from ending up in the middle of the page. Not that big of a deal for one page, but the last page looks odd without this.
    [[pInfo dictionary] setValue:[NSNumber numberWithBool:YES] forKey:NSPrintHeaderAndFooter]; // Add header and footer to all pages
    [[pInfo dictionary] addEntriesFromDictionary:printSettings]; // Add any settings that were passed in.
    NSTextView *printView = [[[NSTextView alloc] initWithFrame:[pInfo imageablePageBounds]] autorelease]; // Create a text view with one page as its size
    printView.jobTitle = @"Expense Report"; // The name used as default for save as PDF and for the default header
    MSTablePrint *tPrint = [[[MSTablePrint alloc] init] autorelease];
    // Set up the fetch request to retrive all Expense entities
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Expense" inManagedObjectContext:[self managedObjectContext]];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entity];
    NSError *anError = nil;
    NSArray *items = [[self managedObjectContext] executeFetchRequest:request error:&anError];
    NSArray *headerArray = [NSArray arrayWithObjects:@"Date", @"Amount", @"Description", @"Category", nil]; // create the first row of the text table with headers
    NSMutableArray *itemArray = [[[NSMutableArray alloc] init] autorelease]; // create the array we will be passing to MSTablePrint
    [itemArray addObject:headerArray]; // whatever is at index 0 of this array will become the first row so we add the headers here
    for (Expense *z in items) { // Step through each Expense entity in the array and create an array to represent the entity.
        [itemArray addObject:[self rowArrayForItem:z]];
    }
    NSAttributedString *atString = [tPrint attributedStringFromItems:itemArray]; //create the text table
    [[printView textStorage] setAttributedString:atString]; // set the text table as the only text in the text view
     NSPrintOperation *printOp = [NSPrintOperation printOperationWithView:printView printInfo:pInfo]; // Setup the actual print operation
     return printOp;
}
You can, of course, leave out the comments as they are really just to explain what each line is doing.
Now if you Build and Go you can print whatever document you want. Try adding lots of entries to a document and you will see that page breaks go around rows, not through them. You can do all the extra customizations you want to the text table before you actually create the string if you want things like red text, or a particular font.

Headers and Footers

Adding the default header and footer is super easy to do. All we have to do is add one line of code to printOperationWithSettings: error: that will tell the system to add a header and footer to all pages for us:

    [[pInfo dictionary] setValue:[NSNumber numberWithBool:YES] forKey:NSPrintHeaderAndFooter];
Now if we Build and Run then select Print we will see in the preview that there is a header that says ‘Untitled’ on the left and has the date and time on the right. On the bottom right is the page number and total page count. This is a perfectly fine header for most things except part that says ‘Untitled’ we should do something about that.
Adding a more descriptive title to the header will take a little more work since our text view doesn’t have a window for the system to pull the title from. NSView has a method  printJobTitle that tells the system what to use in the header and as the default file name for saving as a PDF file. This method normally looks for things like the window title, however our text view is not part of a window so this method just returns ‘Untitled’. Currently NSView does not have a setter for printJobTitle so we will have to subclass NSTextView and add one ourselves.
Go ahead and create a new Objective-C subclass of NSObject and call it PrintTextView. In PrintTextView.h add:
@interface PrintTextView : NSTextView {
    NSString *printJobTitle;
}
@property (copy, readwrite) NSString *printJobTitle;
@end
In PrintTextView.m we just need to synthesize printJobTitle and of course release it in dealloc so this is all we need to add:
@implementation PrintTextView
@synthesize printJobTitle;
- (void)dealloc {
    // Clean-up code here.
    [printJobTitle release];
    self.printJobTitle = nil;
    [super dealloc];
}
@end
Now that we have our new subclass of NSTextView we just need to make a couple changes in MyDocument.m to use it. First off, we need to import PrintTextView.h so the complier doesn’t get upset. Then, in printOperationWithSettings: error:, we need to change one line of code and add one line.
Change:
     PrintTextView *printView = [[[PrintTextView alloc] initWithFrame:[pInfo imageablePageBounds]] autorelease]; // Create a text view with one page as its size
Add:
     printView.printJobTitle = @"Expense Report";
You can, of course, set printJobTitle to whatever string you want, perhaps pull the name of the document and use that [self displayName], but since the print out really is an expense report why not call it one.
Now if you build and run you will see that ‘Untitled’ has changed to ‘Expense Report.’

That’s all folks

That brings us to the end of our time together today. I’ don’t know when I will have the next installment out for you, or even what it will cover, if you have any topics you would like to see covered here please let me know in the comments.
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:
As always, I look forward to your comments, questions, complaints, suggestions, etc.

Printing Tabular Data

July 31, 2010

So I have been working on a Core Data application for Mac OS X that will eventually have an iPhone and iPad. I have been learning how to write software in general and Cocoa code specifically along the way. A few days ago I started working on printing so I did what I always do when I need to learn a new piece and Google it. I found that there was very little out there on the subject, further what was there was fairly vague and mostly boiled down to ‘use WebKit and print that.’ I eventually found a little info that lead me to NSTextTable. Since I foresee making several applications that will need similar printing I decided to abstract as much code as possible out into it’s own class. My goal was to have a class that I could just hand over an array of objects (Core Data or not) and get a table back. I quickly discovered that I would have to send so much information to the class that it would be ridiculous, so I changed to an idea that just used strings.
I ended up with a class that I just had to create an array of arrays of strings to get an attributed string with a text table in it.
Since there is no little out on the web on this subject and I have gotten so much help from the web on some many other subjects I have decided to put my new class MSTablePrint out in to the world for anyone to use. I have given it the same license the Matt Gemmell uses for his source code (I literally copied it from his site and swapped out my name for his). This means that you are free to use this code in any project you want so long as you acknowledge my contribution. If you want for of a description just check out http://mattgemmell.com/license/ (Since I ‘borrowed’ the license from Mr. Gemmell I might as well link to his explanation (also feel free to check out the rest of his site while you’re there, he is _way_ smarter than me with this stuff).

To use this class you need to grab whatever objects you want in your table. Then create an array of strings from the keys you are interested in, in the order you want them to appear (the string at index 0 will end up in the first column). Once the strings are in an array add that array to another array. Whatever array is at index 0 will become the first row so if you want headers you should make them the first array. There are numerous other options you can set such as alternating row colors and header row styles that are all covered in the read me file in the zip file.

I will likely put up a tutorial in a few days using the Expenses application, but in the mean time feel free to post questions here so others can share in the answers.

MSTablePrint

WWDC 2010 Session Breakdown

April 28, 2010

So the information for WWDC came out today (with a whopping 5 weeks notice) and it struck me, as it has many people judging by Twitter, that this year Mac OS X is the red headed step child more so than ever. To demonstrate this I went through all the sessions, of which there are 66 this year, and determined if they were just for iPhone OS, just for Mac OS X, for both, or weren’t really specified. I did this overall and by category and here are the resulting charts:

As you can see almost every session applies to iPhone OS in some way and very few apply to only Mac OS X. If this is any indication then even the sessions that apply to both OSes will mostly focus on iPhone OS. The question now is, is it really worth it for non-iPhone developers to bother going this year?

Troubleshooting tips part 1: NSLog, etc.

January 31, 2010

In the course of working on part four of the Expenses tutorial I kept getting the error about the store being incompatible with the store used to create the file despite the fact that I copied the code over from a working project. I of course started with the generic “Clean All” option, but that didn’t do me any good. As I have never had any formal programming classes (other than a year in Middle school where we had to make a flowchart for how to make a cheeseburger and wrote everything in BASIC, and I don’t mean Visual Basic, I mean late 1980’s BASIC) I don’t really understand how debuggers work, I can do some really basic stuff with the Xcode one now, but nothing of substance. After I was convinced that I had done everything right (after all I’m perfect, right?) I added a single NSLog to the start of the newly overridden method NSLog(@”configuring store.”);. Then I hit build and run, and was confronted by the lack of any statements in the log about configuring store. I was of course at once convinced that there must be something wrong in the system so I, again, did the default “Clean” thinking that would solve it, still no message in the log. I tried a different machine, still no log message. I then went hunting in the docs to see if the method had been deprecated, but it hadn’t been. In desperation I copied the method from the documentation and pasted it into my code, suddenly I had a log message and it all worked. I, of course, was confused and so looked back through the part of the tutorial with the method in it, to see if I miss a part of the method or something. It looked right… until I finally looked letter by letter and saw that I had mis-spelled coordinator by leaving out an ‘o’ the odds of me mis-spelling words is pretty high (I suck at spelling) so I should have looked for that kind of thing sooner, but I thought I had pulled from working code.
As I have been learning to write software these kinds of things have happened less often, somewhat due to experience but mostly because I have learned ways to work with my ignorance.I use NSLog all over the place to help find where things go off the tracks. I have logged each step of a method that was giving me issues before, putting in the values of variables and counts of arrays to see if what I designed to have happen actually happens. This of course means that I have to get rid of all those NSLogs at some point, or have my customer’s console fill up with crap every time they run my app (assuming, of course that I will have more than just the one small Mac app and two small iPhone apps out one day). There are several approaches out there to making this easy, I tend to go really low tech and just #define MSLOG to be either ‘NSLog’ or ‘//’ Depending on what kind of build I am doing. I haven’t used much in the way of automated defines or used scripts in my build process as of yet, but I am now starting to learn such things. So far I have a simple script that increments the build number each time (I got the basic script from a blog whose name now escapes me and altered it to work the way I wanted).
One of the other things I have started doing is keeping a notes file in each project so I can keep track of things right in the project. I am now starting to work on a way to split this notes file out of the app but in the same folder. The idea behind the notes file is that I put in more info than would normally go into a commit message, while the commit message might say something like adds feature x, the notes file will say how feature x was added so I can do it again in a different project if I want. THe notes file also gives me a place to record ideas that didn’t work so I don’t try to get a feature to work the same wrong way five different times. This is really useful for me since I tend to have multiple projects of similar idea at the same time and may go months between times I work on one of them, generally I have one that is my real project and one that is my sandbox to try ideas out. Once I get an idea to work in the sandbox I move it over to the main project using the notes file as a guide for what I need to copy and paste over and what IB connections and bindings are needed.
Let me know what you all have come up with to make your coding lives easier. As I come up with other ideas, possibly based on some of your comments, I will post about them as well.

Expenses: A Core Data Tutorial Part 4, Versioning

January 27, 2010

Basic Data Versioning

This time we are going to be working with some basic data migration. In OS X 10.4 & 10.5 data migration was a bit tricky (mostly because the Apple docs weren’t very clear about a method you had to add to make all the pieces work). OS X 10.6 has made things a bit easier. We will cover the differences when we get there.

As before here is a link to the project as we left it at the end of our last edition.

Expenses3.zip

Creating the new model

To start with we need to create a new data model file. Select the current data model, then select Design -> Data Model -> Add Model Version.

Add Model Version

This will create a copy of the data model file named “MyDocument 2.xcdatamodel.” if you look you will also notice that in the Groups & Files list there has been a change where the original file was, it now has a disclosure triangle and a new extension “.xcdatamodeld” this is the directory that all the versions of the model file will go. The original file also has a green check mark on it’s icon now to indicate that it is the current version of the data model. You can either edit the original or duplicate file, both will work. I prefer to edit the duplicate file so I can keep track of things. Select the new model file then Design -> Data Model ->Set Current Version. For this project just add an attribute named “notes” to the Expenses entity with a type of string.

Adding Notes Attribute

Save the new model file. To see why I always edit the duplicate file select the data model and create another new version, you will see that it is number 3 and is a copy of version 2. All the versions of the model file are sorted in order so it is very easy to see which one is the newest one. Of course for a commercial app you may want to name data models based on the app version so you know what version has what data model for support and the such. You can delete version 3 of the model file as we won’t be using it.

GUI Changes

In order to actually use our new notes attribute we need to make some changes to the GUI. First, open MyDocument.xib and select the Expenses tab. lengthen the window, tab view, and box to allow for the addition of an NSTextView. Drag over an NSTextView sizing it as needed. Click on the text view as needed to actually get the NSTextView selected. In the inspector turn off rich text and then bind the Value to: ExpenseView Array Controller.selection.notes. You could just save and close at this point, but first look at the Categories tab. Lots of dead space down there, perhaps we should do something about that. For a simple fix just lengthen the two table views, moving things down as needed, to take up the extra space. Now, save and close.

The New GUI

Turning on Migration

The thing that the Apple documentation was always a little lacking on in OS X 10.4 & 10.5 was actually turning on the migration, It took me forever the first time I tried to do any migration to actually make things work before I found this little tidbit. We have to override  – (BOOL) configurePersistentStoreCoordinatorForURL: (NSURL *)url ofType: (NSString *) fileType modelConfiguration: (NSString *) configuration storeOptions: (NSDictionary *) storeOptions error: (NSError **)error in MyDocument.m to let the system know it should migrate the data. Here is where things diverge a bit for OS X 10.4 & 10.5 versus 10.6. For the older versions we use the following code along with a mapping model, which we will cover in a moment.:

– (BOOL) configurePersistentStoreCoordinatorForURL: (NSURL *)url ofType: (NSString *) fileType modelConfiguration: (NSString *) configuration storeOptions: (NSDictionary *) storeOptions error: (NSError **)error

{

NSMutableDictionary *options = nil;

if (storeOptions != nil)

{

options = [storeOptions mutableCopy];

}

else

{

options = [[NSMutableDictionary alloc] init];

}

[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];

BOOL result = [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:options error:error];

[options release], options = nil;

return result;

}

With a migration this simple, just adding an attribute to an entity, in Snow Leopard we use code that looks just like above with just the addition of a second key in the dictionary set to yes that will save us making a mapping model.:

– (BOOL) configurePersistentStoreCoordinatorForURL: (NSURL *)url ofType: (NSString *) fileType modelConfiguration: (NSString *) configuration storeOptions: (NSDictionary *) storeOptions error: (NSError **)error

{

NSMutableDictionary *options = nil;

if (storeOptions != nil)

{

options = [storeOptions mutableCopy];

}

else

{

options = [[NSMutableDictionary alloc] init];

}

[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];

// Add this line for simple migrations in Snow Leopard

[options setObject:[NSNumber numberWithBool:YES] forKey: NSInferMappingModelAutomaticallyOption];

BOOL result = [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:options error:error];

[options release], options = nil;

return result;

}

Mapping Model

A mapping model tells the system how to get from one version of a data model to the other. For basic migrations we only need a mapping model for OS X 10.4 & 10.5, for more advanced migrations we need a mapping model under OS X 10.6 as well. For this project the mapping model will be very basic. We start by creating a new mapping model file (File-> New File…), this changed from OS X 10.5 to 10.6 so both views are shown: (Note that if you aren’t presented with the mapping model option cancel, select the data model and try again.)

Mapping Model in 10.5

OS X 10.5

Mapping Model in 10.6

OS X 10.6

I named mine “ver1to2” but the name is fairly unimportant. After you name the file you will be asked for the source and destination files. Select the original model for the source and the new model for the destination.

Source and destination for the mapping Model

Since all we did was add one attribute the default mapping model will suffice. Xcode will look at both models and see that there is only one additional attribute and map all the pre-existing entities and attributes to the proper destinations. If you wanted to fill the notes attribute with some sort of migrated from old store message you could do it in this file.

If you hadn’t overridden – (BOOL) configurePersistentStoreCoordinatorForURL: (NSURL *)url ofType: (NSString *) fileType modelConfiguration: (NSString *) configuration storeOptions: (NSDictionary *) storeOptions error: (NSError **)error you would get something like this (I got this a lot the first time I tried to do migration):

Of course you have overridden that method so you won’t have that problem.

That’s all folks

That brings us to the end of our time together today. I’m pondering what we should do in the next edition, but hopefully it will be something interesting. 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:

Expenses4.zip

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


Expenses: A Core Data Tutorial part 3

June 23, 2009

User Preferences

So it has taken far longer than I anticipated to get this one out, but I suppose these things happen. This time we will be setting up some user defaults, we will be doing far less with Core Data specific stuff but a lot of this stills ties into Core Data.

Preferences we will add:

  • When we launch should we open a blank document, nothing, or the last edited document.
  • How far from the current date should expenses default to.
  • Are there any categories the user would like added to all new documents.

If you skipped the first two tutorials and would rather not do them here is a link to the project as of the end of part 2.

Expenses Part 2

We will be making a few new files today, if you want to create them all at once here is the list:

  • externs.h (Empty File)
  • AppController.h &.m (Objective-C class)
  • PreferenceController.h & .m (Objective-C NSWindowController subclass)
  • Preference.xib (Window XIB)

I am not going to go into huge detail about how NSUserDefaults works since numerous others have done an excellent job, including my two favorite places, CocoaDevCentral and Aaron’s book, Cocoa Programming for Mac OS X. We are going to go a bit beyond what is currently covered in either of those places though.

The externs file

So whenever I am going to have lots of externs I like to put them all in one file so they are easy to find when I forget what they are called. Then I just have to import them into each class file to use them. We will define several externs that will be the keys to our user defaults dictionary.

Create a new file that is a blank file, and name it “externs.h” then make it look like this:

/*

externs.h

This is just a collection of all externs for the project.

*/

// To determine if we should oopen a blank document at launch

extern NSString * const ExpEmptyDocKey;

// To determine if we should open the last opened document at launch

extern NSString * const ExpOpenLastDocKey;

// To determine if we should insert the default categories to new docs

extern NSString * const ExpDefaultCategoriesKey;

// The list of what categories should get added if above is YES

extern NSString * const ExpDefaultCategoriesList;

// How far before the current date should expenses start at.

extern NSString * const ExpDefaultDateOffset;

Don’t forget to save!

AppController part 1

Now we need to set the ‘factory’ defaults. To do this we need to edit the AppController class a bit. If you have not already created the files do so now. Then, in AppController.h add the following above @interface :

#import “Externs.h”

NSString * const ExpEmptyDocKey = @”EmptyDocumentFlag”;

NSString * const ExpOpenLastDocKey = @”OpenLastDocFLag”;

NSString * const ExpDefaultCategoriesKey = @”DefaultCategoriesFlag”;

NSString * const ExpDefaultCategoriesList = @”DefaultCategoriesList”;

NSString * const ExpDefaultDateOffset = @”DefaultDateOffset”;

After that, in AppController.m add this code: (I’m not really happy with all of this code, but I haven’t found a better way to deal with the KVC issues in IB without using dictionaries)

@implementation AppController

+ (void) initialize

{

// create a dictionary for the ‘factory’ defaults

NSMutableDictionary *defaultValues = [NSMutableDictionary dictionary];

// First to create an array of default Categories (for now we will just make a few)

NSMutableDictionary *catOne = [[[NSMutableDictionary alloc] initWithCapacity:1] autorelease];

NSMutableDictionary *catTwo = [[[NSMutableDictionary alloc] initWithCapacity:1] autorelease];

NSMutableDictionary *catThree = [[[NSMutableDictionary alloc] initWithCapacity:1] autorelease];

NSMutableDictionary *catFour = [[[NSMutableDictionary alloc] initWithCapacity:1] autorelease];

NSMutableDictionary *catFive = [[[NSMutableDictionary alloc] initWithCapacity:1] autorelease];

[catOne setValue:@”Housing” forKey:@”theString” ];

[catTwo setValue:@”Food” forKey:@”theString” ];

[catThree setValue:@”Entertainment” forKey:@”theString”];

[catFour setValue:@”Misc” forKey:@”theString”];

[catFive setValue:@”Transportation” forKey:@”theString”];

NSArray *catArray = [NSArray arrayWithObjects:catOne, catTwo, catThree, catFour, catFive, nil];

// add defaults to dictionary

[defaultValues setObject:[NSNumber numberWithBool:YES] forKey:ExpEmptyDocKey];

[defaultValues setObject:[NSNumber numberWithBool:YES] forKey:ExpDefaultCategoriesKey];

[defaultValues setObject:[NSNumber numberWithBool:NO] forKey:ExpOpenLastDocKey];

[defaultValues setObject:[NSNumber numberWithInt:0] forKey:ExpDefaultDateOffset];

[defaultValues setObject:catArray forKey:ExpDefaultCategoriesList];

// register the defaults

[[NSUserDefaults standardUserDefaults] registerDefaults:defaultValues];

}

PreferenceController

Now that we have created the standard defaults we need to be able to edit them. to start with we need to do a little editing to the PreferenceController class (again if you haven’t yet, create the files). In PreferenceController.m you just need to add an init method to return the correct nib file:

– (id) init

{

if (![super initWithWindowNibName:@”Preferences”])

{

return nil;

}

return self;

}

AppController part 2

Back in AppController.h we need to add one outlet and one method:

@interface AppController : NSObject

{

PreferenceController *preferenceController;

}

– (IBAction) showPreferencePanel:(id)sender;

@end

Then in AppController.m we need to implement the method:

– (IBAction) showPreferencePanel:(id)sender

{

if (!preferenceController)

{

preferenceController = [[PreferenceController alloc] init];

}

[preferenceController showWindow:self];

}

Of course we need to hook up our new method to make it work. Open MainMenu.xib in IB, and add an NSObject from the ‘Objects & Controllers’ part of the Library (just drag it over to the IB document widow to add it). In the Inspector select the Idenity tab set the class to ‘AppController’ (if for  some reason it doesn’t show up make sure you have saved the AppController.h file). Then, select the application menu (the one with the apps name) and control drag from the ‘Preferences’ menu item to the AppController and select showPreferencePanel: as the action.

Preferences.xib

We are going to take care of setting preferences with an NSUserDefaultsController and bindings. To start with, if you haven’t already, create the Preferences.xib file, then open it in IB. Drag out the necessary controls to make it look something like this:

PreferencesXIB

To create the ‘+’ and ‘-’ buttons I used the NSAddTemplate and NSRemoveTemplate as the images.

Next, in IB,  drag over an NSUserDefaultsController to the Preferences.xib document. Also drag over an NSArrayController. In the inspector for the user defaults controller uncheck the “Applied Immediately.” checkbox (if you prefer you can leave it checked and changes to the user defaults will be saved as soon as they are changed. We will start bindings from the top down, so let’s start with the checkbox labeled “Open a new document.” Bind the value to User Defaults Controller.values.EmptyDocumentFlag.

firstPrefBinding

You may have noticed that we used the actual value of the string we declared as one of our externs not the extern variable, this is because IB has no knowledge of the externs so we have to use the values of the variables. In order to save problems later I would suggest using copy/paste to make sure you don’t mistype anything. The rest of the bindings should be pretty obvious, but here is the connections HUD for the User Defaults Controller. You should note that the DefaultCategoriesList gets bound to the array controller, this is so we can get add and delete for free. One really important thing to note is that the DefaultDateOffset needs to be bound to both the text field and the stepper, otherwise it won’t work right.

BindingsHUD

To get the table view to show the default categories select the table column (make sure you select the  column and not the scroll view or table view) and bind its value to the arranged objects of the array controller and set the key to “theString.” Then we need to hook-up all the actions. Control drag from the add button to the array controller and select the add: method do the same with the remove button (connecting it to remove: of course). After that, control drag from the save button to the user defaults controller and select the save: method, the restore defaults button will get connected to the revertToInitialValues: method. For some reason that I do not understand the revertToInitialValues: method does not actually work, the revert: method works great to delete unsaved changes and save: works as it should as well, if anyone has any ideas on what is missing please let me know.

Don’t forget to save everything, Next we will move on to implementing those preferences.

Implementing the preferences

Let’s start with the easiest one, opening a blank document on startup. To start with the AppController needs to be the app delegate in order to get the methods calls we will need. You can either do this by implementing the init method and then calling [NSApp setDelegate: self] or open MainMenu.xib in IB, control drag from File’s Owner to the App Controller instance and select delegate as the outlet. Then in AppController.m add the following method:

– (BOOL) applicationShouldOpenUntitledFile:(NSApplication *) sender

{

return [[NSUserDefaults standardUserDefaults] boolForKey:ExpEmptyDocKey];

}

That takes care of our first preference, now on to opening the last document that was opened when launching. This also takes advantage of one of the methods that gets called on the app delegate, so again in AppController.m add the following method:

– (void)applicationDidFinishLaunching:(NSNotification *)aNotification

{

// see if we need to open the most recent document or not and do it if needed

if ([[NSUserDefaults standardUserDefaults] boolForKey:ExpOpenLastDocKey])

{

NSArray *anArray;

NSDocumentController *docController = [NSDocumentController sharedDocumentController];

anArray = [docController recentDocumentURLs];

if (anArray != nil && [anArray count] > 0)

{

NSError *anError = nil;

[docController openDocumentWithContentsOfURL:[anArray objectAtIndex:0] display:YES error:&anError];

}

}

}

On order to implement the default date offset we are going to do things a little differently (we may be straying from MVC a bit here, but I think it works). This is the one time we will code the actual value of one of our externs, we are doing this so that the entity class can be reused in other apps more easily (we can declare a different extern in another app but define it the same way and have use of this method there). In Expense.h modify the awakeFromInsert method to look like this:

– (void) awakeFromInsert

{

NSNumber *days = [[NSUserDefaults standardUserDefaults] objectForKey:@”DefaultDateOffset”];

NSLog(@”days = %@”, days);

int dayInt = [days intValue];

NSLog(@”dayInt = %i”, dayInt);

if (dayInt == 0)

{

NSDate *now = [NSDate date];

self.date = now;

}

else

{

int seconds = dayInt * 86400; // number of days times number of seconds in one day.

NSDate *theDate = [[NSDate alloc] initWithTimeIntervalSinceNow:seconds];

self.date = theDate;

// we called alloc therefore we call release, even though it will be released when this method ends anyway.

[theDate release];

}

}

For our final trick we will create some default categories for the user in all new documents. There are several steps involved in this but it all happens in one method in MyDocument. We are going to override the  method since it only gets called when new documents are created. Before we start adding categories to the document we will first need to disable undo tracking so the document doesn’t start out dirty, then we add the categories, process the changes, and re-enable undo tracking. I’ve commented the code here pretty well so you can follow along with what is happening easier. In MyDocument.m add the following method definition:

// Called only when document is first created

– (id) initWithType: (NSString *) typeName error: (NSError **) outError

{

// call the designated initalizer

MyDocument *document = [self init];

// pass on the file type

[self setFileType:typeName];

// disable undo tracking

NSManagedObjectContext *context = [self managedObjectContext];

[[context undoManager] disableUndoRegistration];

// check if the user want new documents to have basic entities added

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

if ([defaults boolForKey: ExpDefaultCategoriesKey])

{

// if the user wants the default items we hand them over

// Add the default entities

NSArray *categoryArray = [defaults objectForKey: ExpDefaultCategoriesList];

NSDictionary *dictionary;

for (dictionary in categoryArray)

{

Category *newCategory = [NSEntityDescription insertNewObjectForEntityForName: @”Category” inManagedObjectContext: [self managedObjectContext]];

[newCategory setValue: [dictionary objectForKey:@”theString”] forKey: @”name”];

}

}

// enable undo tracking

[context processPendingChanges];

[[context undoManager] enableUndoRegistration];

return document;

}

That’s all folks

That brings us to the end of our time together today. In our next session we will do some versioning, and if I can figure out we will add in some other features as well. 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:

Expenses3.zip

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.

Xcode haters

June 21, 2009

So I use TweetDeck for my twitter account (@michaelswan) and keep searches open for Xcode, Cocoa, CoreData, & Core Data. I have been noticing on the Xcode search that there are a lot of Xcode haters posting with what amounts to “Xcode sucks compared to ________.” Generally the blank is filled with Visual Studio and sometimes Eclipse. I have never used any other IDE, except a brief run in with Think C way back when I first tried to learn programming (that attempt btw failed miserably), so I really have no point of comparison. I have to wonder however is it really that Xcode is not as good as Visual Studio or Eclipse, or is it simply that now the iPhone is out and everyone is trying to get rich quick off of it (I really do think the iPhone is the modern California Gold Rush) that lots of Windows developers are coming over and can’t wrap their heads around the Mac way of thinking. As a side note before this turns into a which OS is better battle; I love the Mac OS, have never owned anything that wasn’t Mac (Classic II in 1992), and I hope to never touch a Windows machine again. That said I don’t think that Windows users are deficient in any way, they just think in a different way than me.
With this in mind I invite you all to leave comments with specific deficiencies in Xcode compared to other IDEs. You should know that I will just delete comments that just say that Xcode sucks, give me a real reason why it sucks. In the end I will try to take the comments boil them down to a consolidated list and submit an enhancement request to Apple, after all, just saying something sucks doesn’t fix it, isolate the problems and ask for them to be fixed.

A Core Data Tutorial Part 2: Polishing the Basics

May 30, 2009

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.