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.

theMIkeSwan

Entertainment Lighting Tech, OS X & iOS developer

3 Comments:

  1. Congratulations!
    one thing: can I use your code with a Core data – non document App?

    Thanks
    Mario

    • Mario, It should work for non-document based apps as well. I think the method for getting into printing may be a bit different but the process of creating the text table to print should work just the same.

  2. I have been checking out a few of your posts and it’s pretty good stuff. I will definitely bookmark your website.

Leave a Reply