Expenses Part 5: Printing Core Data

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.
Advertisements

8 Responses to “Expenses Part 5: Printing Core Data”

  1. corepitufo42 Says:

    Thank you very much. This explanation is really helpful, in fact is the best explanation I’ve ever read.

  2. michael Says:

    I am trying to find a way to implement the same functionality on an iPad? is there anything in your code which won’t work on iOS?

  3. Ali Altemimy Says:

    How can you customize the column width in print view
    Thanks

  4. Ali Al temimy Says:

    I am still waiting for your kind reply about customize the column width separately

    Many thanks

    • theMikeSwan Says:

      My apologies for not responding sooner however this blog is no longer active I have moved everything to theMikeSwan.com/blog and pay much more attention to what is happening there.

      If you look at the header file for MSTablePrint (or TMSTablePrint if you use the more updated version I have on GitHub in TMSSupportClasses) you should see two properties, one is a BOOL flag that says if the table should use column percentages or not and the other is an NSArray of the desired percentages. The object at index 0 will be used for the first column.

      You can also grab the table directly through the table property so that you can make any adjustments directly to the NSTextTable that you want, just make the adjustments after you have created the TMSTablePrint object and before you get the NSAttributedString back.

    • Ali Al temimy Says:

      Many thanks to you for your help I appreciate

  5. zenlifefrugal Says:

    I am having trouble implementing this in Xcode 4.6.2. I keep getting something about it being for 32 bit computers, rather than 64 bit.

Leave a Reply

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

WordPress.com Logo

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

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s


%d bloggers like this: