Support classes

Just a quick post to let you know that I created a repo on GitHub of support classes. It includes the table printing classes I talked about previously as well as a class for exporting (and eventually importing) CSV data and classes for iCloud. You can find it here: https://github.com/theMikeSwan/TMSSupportClasses

I will be adding other classes as I create them and I will generally add a tutorial here sometime after that.

You are welcome to fork the repo as much as you want and I would love to merge in any improvements you come up with.

Posted in Programming, Source Code | Leave a comment

Expenses: A Core Data Tutorial Part 1 with Xcode 4

Since Xcode 4 has, at least for the moment, removed the wonderful Core Data UI prototyping tool here is a newer version that goes through setting up the same app as before but using Xcode 4.1 running on Lion. While the screen shots will all be from 4.1 on Lion almost everything will be the same if you are running 4.0.x on Snow Leopard.

To save you from the need to first read the original tutorial before this one I will cover everything from the last one here just adding in the bits required for Xcode 4.x. If you have already completed the previous version of this tutorial (or for whatever other reason just need the portion related to bindings) feel free to scroll down until just past the image of a Commit sheet (at that point the project has been created and the data model built). If you find you are having issues using Xcode 4 then I suggest you take a look at these guides to start with:

For a complete list of guides related to Xcode check out Tools & Languages (you don’t even need a login to get to these).

With that out of the way let’s begin:

This tutorial assumes you have the Apple Developer Tools installed and are familiar with basic operation of Xcode and Interface Builder. It also assumes that you have a very basic knowledge of Core Data. If you are completely new to Core Data check out either Getting Started with Core Data or read chapters 11 & 30 of Aaron Hillegass’s book Cocoa Programming for Mac OS X 3rd Edition. Really if you are new to Cocoa you should have a copy of Aaron’s book no matter what, it is the one book I always go to when I’m not sure about something. We will also be doing a little with bindings, if you need a starter check out either Cocoa Bindings Programming Topics or Chapter 11 of Aaron’s book.

This is the first tutorial in the series so we should first specify our goal. The purpose of the app will be to track daily expenses. Every feature we think about adding should be assessed based on that one sentence, this will hopefully help keep down the feature creep. Since the purpose of the app is to track expenses we will just call it Expenses. Now we should determine the major features of the app. It will of course need to store expenses, perhaps when, what, & how much. To make life easier it would be a good idea to be able to assign each expense a category so the user can figure out things like how much they spend on food versus movies. Of course along the way we will come up with a few smaller features (such as copy and paste of expenses) that we will add in, but this should be the bulk of it. We will use Core Data to build Expenses because it is extremely well suited to this kind of work and will reduce the amount of code we have to write dramatically.

Note: The original version of this tutorial had the idea of also covering things like crash reporting, updates, etc.; however now that the Mac App Store has been released those things are handled by the store for you. I may cover a very basic validation method for App Store apps but if you look on GitHub you will find the code I would use (I would recommend making some changes to what you find).

Today we are going to start by just building the basic app without writing any code at all!

Continue reading

Posted in Core Data, Programming | 20 Comments

Expenses Part 5: Printing Core Data

 

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.

Expenses4.zip

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 NSTextTableBlock

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:

MSTablePrint.zip

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 substitutions 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:

Expenses5.zip

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

Posted in Core Data, Programming | 3 Comments

Printing Tabular Data

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

Posted in Core Data, Programming, Source Code | Leave a comment

Troubleshooting tips part 1: NSLog, etc.

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.

Posted in Programming | Tagged | Leave a comment

Expenses: A Core Data Tutorial Part 4, Versioning

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.

Posted in Core Data, Programming | Leave a comment

Expenses: A Core Data Tutorial part 3

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. 
  */ 

 extern NSString *const ExpEmptyDocKey;           // To determine if we should oopen a blank document at launch
 extern NSString *const ExpOpenLastDocKey;        // To determine if we should open the last opened document at launch
 extern NSString *const ExpDefaultCategoriesKey;  // To determine if we should insert the default categories to new docs
 extern NSString *const ExpDefaultCategoriesList; // The list of what categories should get added if above is YES
 extern NSString *const ExpDefaultDateOffset;     // How far before the current date should expenses start at.

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: 06] 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 = [docControllerrecentDocumentURLs];  
		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"]; 
	int dayInt = [days intValue]; 
	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.

Posted in Core Data, Programming | Leave a comment

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.

Posted in Core Data, Programming | Leave a comment

A Core Data Tutorial Part 1

This is the first in a series of tutorials that combines other tutorials I have seen and in the end will go a little past the others I have seen. My intention is to take this app from beginning to end, including the finishing touches like update checking, serial number, and icons. I am writing this to help me figure out a few things about Core Data and hopefully help others as well.

This tutorial assumes you have the Apple Developer Tools installed and are familiar with basic operation of Xcode and Interface Builder. It also assumes that you have a very basic knowledge of Core Data. If you are completely new to Core Data check out either Build a Core Data App or read chapters 11 & 30 of Aaron Hillegass’s book Cocoa Programming for Mac OS X 3rd Edition. Really if you are new to Cocoa you should have a copy of Aaron’s book no matter what, it is the one book I always go to when I’m not sure about something. We will also be doing a little with bindings, if you need a starter check out either Cocoa Bindings Intro or Chapter 11 of Aaron’s book.

This is the first tutorial in the series so we should first specify our goal. First off the purpose of the app will be to track daily expenses. Every feature we think about adding should be assessed based on that one sentence, this will hopefully help keep down the feature creep. Since the purpose of the app is to track expenses we will just call it Expenses. Now we should determine the major features of the app. It will of course need to store expenses, perhaps when, what, & how much. To make life easier it would be a good idea to be able to assign each expense a category so the user can figure out things like how much they spend on food versus movies. It would also be good to show how much was spent in a month on any one category, as well as overall (this is a feature I will need some help with). Of course we will also show yearly totals both by category and overall. We are also going to want to let the user know when we release new version of the app without having to e-mail them (they don’t want the e-mail and will ignore it). Since we are putting all this effort into writing this Expenses perhaps we want to get paid for it, in which case we will want some way to require a serial number for real use of the app. Finally in order to improve the user experience we should get crash reports so we can figure out how to reduce (if not eliminate) them. Of course along the way we will come up with a few smaller features (such as copy and paste of expenses) that we will add in, but this should be the bulk of it. We will use Core Data to build Expenses because it is extremely well suited to this kind of work and will reduce the amount of code we have to write dramatically.

Today we are going to start by just building the basic app without writing any code at all!

To start with open Xcode and start a new Document based Core Data application.

New Project

Name it Expenses (or whatever you want to call it).

Next set up the model as below.

Attributes:

Expense

Category

Name

Type

Name

Type

desc

string

name

string

date

date

amount

float

Relationships:

Expense

Category

Name

Destination

Name

Destination

category

Category

expenses

Expense (to-Many)

In the end the model should look like this:

Picture 4

Make sure you save.

Next let’s build the user interface.

Open MyDocument.xib

Remove the Label.

Add a tab view.

Size it to take up most of the window.

TabView

In the inspector select the size tab and set the springs and struts so that the tab view resizes with the window.

Size Inspector

Label one of the tabs “Expenses” and the other “Categories.”

Tab Labels

Make sure the Expenses tab is active, then drag over a Core Data Entity from the IB Library. It will bring up the “New Core Data Entity Interface” assistant:

Select Expenses -> MyDocument -> Expense as below, then click next.

Entity 1

In the next pane select “Detail/Master View” and select “Detail Field” & “Add/Remove.” If you want you can also select “Search Field” to get basic searching for free.

Entity 2

In the last pane select all of the attributes and the one relationship:

Entity 3

You should have something that looks similar to the image below. It needs a little work, first off lets get rid of that extra box (Layout -> Unembed Objects).

Default layout

Next lets make a few other tweaks. Date should really be the first column; Description, the second; followed by Amount, and Category. Amount will need it’s number formatter adjusted to display currency in whatever way you want. You will likely want to adjust the date formatter in the date column (unless you want the really long default format). Then resize the the table and it’s columns to something resembling the image below. Once that is done collect all the detail fields and put them in a box at the bottom. you will also need to adjust the formatters for the date and amount fields to match the ones in the table view (unless you want them to be different of course). In the end it should look something like this:

Expense Layout

Before we do the other tab we should make life easier if we have to change any bindings later. You should have two array controllers in you xib file now, one named “Expense Array Controller” and one named “Category Array Controller.” When you drop the Category entity into the other tab IB will add in another “Category Array Controller” making it very hard to tell which is which. To eliminate this problem rename “Expense Array Controller” to “ExpenseView Array Controller” and “Category Array Controller” to “CategoryPopUp Array Controller” this just makes everything way more clear for the future.

Now select the Categories tab and drop a Core Data Entity on it. follow the same procedure as before (only select the Category entity of course). Re-arrange the interface to look like below. Go ahead and rename the new “Category Array Controller” to “CategoryView Array Controller.”

Category Layout

If you are wondering what that huge empty space is for, it’s for the table view that you need to add now. It should have three columns; Date, Description, and Amount. Below the table view add two labels; “Total:” and a placeholder with a number formatter set to your favorite currency settings (I tend to use “$10,000,000.00” as my place holder since it should always be big enough for any numbers I will ever be entering).

FinalCat

Now to fill that nice new table view with some data. First add a new array controller and name it “ExpenseByCat Array Controller.” Now your xib file should look something like this:

xib file

See how easy it is to figure out which array controller does what.

In the bindings section of the inspector for “ExpenseByCat Array Controller” bind the Content Set (not the Array) to “CategoryView Array Controller.selection.expenses” this will cause the array controller to be filled by the expenses of the currently selected category.

ExpenseByCat Binding

Now that the array controller is hooked up we need to bind the table view columns. Bind each column to “ExpenseByCat Array Controller.arrangedObjects” and set the “Model Key Path:” as needed for each column.

Expense Table Bindings

Then select that placeholder label that you put below the table view earlier and bind it the same as the table view columns (“ExpenseByCat Array Controller.arrangedObjects”) for the “Model Key Path:” though you are going to enter a little magic: “@sum.amount” this will take care of adding up all the amounts of each expense in the category for you.

sumBinding

Build and Go!

CategoryRunningExpenseRunning

You should be able to add and remove Categories and Expenses. Undo should already work. Saving and reverting should work. So should the total for each category, that’s a lot of stuff with out writing any code yet! But there are some things that would be nice to add.

In the next installment we will add:

  • New expenses default to the current date.
  • Alphabetize categories in the popup menu.
  • Make expenses start out sorted by date and keep them that way even when the date is edited.
  • Copy and paste of expenses.

Let me know what you think or if I missed something (after all I am still fairly new at this). Also if you have any ideas about how to sort out how much was spent in each category for each month please let me know.

Posted in Core Data, Programming | 2 Comments