Classes/RecipePrintPageRenderer.m

/*
     File: RecipePrintPageRenderer.m 
 Abstract: A custom UIPrintPageRenderer to render one or more Recipes for printing 
  Version: 1.2 
  
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple 
 Inc. ("Apple") in consideration of your agreement to the following 
 terms, and your use, installation, modification or redistribution of 
 this Apple software constitutes acceptance of these terms.  If you do 
 not agree with these terms, please do not use, install, modify or 
 redistribute this Apple software. 
  
 In consideration of your agreement to abide by the following terms, and 
 subject to these terms, Apple grants you a personal, non-exclusive 
 license, under Apple's copyrights in this original Apple software (the 
 "Apple Software"), to use, reproduce, modify and redistribute the Apple 
 Software, with or without modifications, in source and/or binary forms; 
 provided that if you redistribute the Apple Software in its entirety and 
 without modifications, you must retain this notice and the following 
 text and disclaimers in all such redistributions of the Apple Software. 
 Neither the name, trademarks, service marks or logos of Apple Inc. may 
 be used to endorse or promote products derived from the Apple Software 
 without specific prior written permission from Apple.  Except as 
 expressly stated in this notice, no other rights or licenses, express or 
 implied, are granted by Apple herein, including but not limited to any 
 patent rights that may be infringed by your derivative works or by other 
 works in which the Apple Software may be incorporated. 
  
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE 
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 
  
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 
 POSSIBILITY OF SUCH DAMAGE. 
  
 Copyright (C) 2013 Apple Inc. All Rights Reserved. 
  
 */ 
 
#import "RecipePrintPageRenderer.h"
#import "Recipe.h"
#import <objc/runtime.h>
 
 
/*
 Set up some values for the constant properties of our custom
 recipe printed presentation.
 */
#define HEADER_HEIGHT 20
#define FOOTER_HEIGHT 20
#define PADDING 10
#define TITLE_SIZE 24
#define SYSTEM_FONT [UIFont systemFontOfSize:[UIFont systemFontSize]]
 
@interface RecipePrintPageRenderer ()
 
@property (nonatomic) CGFloat recipeInfoHeight;
 
- (void)setupPrintFormatters;
- (CGRect)contentArea;
- (void)drawRecipe:(Recipe *)recipe inRect:(CGRect)rect;
- (void)drawRecipeImage:(UIImage *)image inRect:(CGRect)rect;
- (void)drawRecipeName:(NSString *)name inRect:(CGRect)rect;
- (void)drawRecipeInfo:(NSString *)info inRect:(CGRect)rect;
 
@end
 
@implementation RecipePrintPageRenderer
{
    NSRange pageRange;
    NSArray *recipes;
    NSMapTable *formatterToRecipeMap;
}
 
/*
 Initialize to our constant values.
 */
- (id)initWithRecipes:(NSArray *)someRecipes
{
    self = [super init];
    if (self) {
        recipes = [someRecipes copy];
        formatterToRecipeMap = [NSMapTable strongToStrongObjectsMapTable];
        self.headerHeight = HEADER_HEIGHT;
        self.footerHeight = FOOTER_HEIGHT;
        self.recipeInfoHeight = 150;
    }
    
    return self;
}
 
/*
 Release ownership.
 */
 
#pragma mark -
 
/*
 Calculate the content area based on the printableRect, that is, 
 the area in which the printer can print content. a.k.a the imageable area of the paper.
 */
- (CGRect)contentArea {
    CGRect r = self.printableRect;
    r.origin.y += self.headerHeight;
    r.size.height -= self.headerHeight + self.footerHeight;
    return r;
}
 
- (void)prepareForDrawingPages:(NSRange)range {
    pageRange = range;
    [super prepareForDrawingPages:range];
}
 
#pragma mark -
 
/*
 This method must be overriden when doing custom drawing as we are. 
 Since our custom drawing is really only for the borders and we are 
 relying on a series of UIMarkupTextPrintFormatter to display the recipe
 content, UIKit can calculate the number of pages based on informtation
 provided by those formatters. 
 
 Therefore, setup the formatters, and ask super to count the pages.
 */
- (NSInteger)numberOfPages {
    self.printFormatters = nil;
    [self setupPrintFormatters];
    return [super numberOfPages];
}
 
/*
 Iterate through the recipes setting each of their html representations into 
 a UIMarkupTextPrintFormatter and add that formatter to the printing job.
 */
- (void)setupPrintFormatters {
    NSInteger page = 0;
    CGFloat previousFormatterMaxY = CGRectGetMinY(self.contentArea);
    
    for (Recipe *recipe in recipes) {
        NSString *html = recipe.htmlRepresentation;
        
        UIMarkupTextPrintFormatter *formatter = [[UIMarkupTextPrintFormatter alloc] initWithMarkupText:html];
        [formatterToRecipeMap setObject:recipe forKey:formatter];
        
        // Make room for the recipe info
        UIEdgeInsets contentInsets = UIEdgeInsetsZero;
        contentInsets.top = previousFormatterMaxY + self.recipeInfoHeight;
        if (contentInsets.top > CGRectGetMaxY(self.contentArea)) {
            // Move to the next page
            page++;
            contentInsets.top = CGRectGetMinY(self.contentArea) + self.recipeInfoHeight;
        }
        formatter.contentInsets = contentInsets;
        
        // Add the formatter to the renderer at the specified page
        [self addPrintFormatter:formatter startingAtPageAtIndex:page];
        
        page = formatter.startPage + formatter.pageCount - 1;
        previousFormatterMaxY = CGRectGetMaxY([formatter rectForPageAtIndex:page]);
        
    }
}
 
#pragma mark -
 
/*
 Custom UIPrintPageRenderer's may override this class to draw a custom print page header. 
 To illustrate that, this class sets the date in the header.
 */
- (void)drawHeaderForPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)headerRect {
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"MMMM d, yyyy 'at' h:mm a"];
    NSString *dateString = [dateFormatter stringFromDate:[NSDate date]];
    
    [dateString drawInRect:headerRect withAttributes:nil];
    
}
 
/*
 Custom UIPrintPageRenderer's may also override this class to draw a custom print page footer. 
 To illustrate that, this class sets the current and total page number in the footer.
 */
- (void)drawFooterForPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)footerRect {
    NSString *footer = [NSString stringWithFormat:@"Page %d of %d", pageIndex - pageRange.location + 1, pageRange.length];
    
    [footer drawInRect:footerRect withAttributes:nil];
}
 
/*
 to intermix custom drawing with the drawing performed by an associated print formatter, this method is called for each 
 print formatter associated with a given page.
 
 We do this to intermix/overlay our custom drawing onto the recipe presentation.
 We draw the upper portion of the recipe presentation by hand (image, title, desc), 
    and the bottom portion is drawn via the UIMarkupTextPrintFormatter.
 */
- (void)drawPrintFormatter:(UIPrintFormatter *)printFormatter forPageAtIndex:(NSInteger)pageIndex {
    [super drawPrintFormatter:printFormatter forPageAtIndex:pageIndex];
    
    /*
     To keep our custom drawing in sync with the printFormatter, base our drawing
     on the formatters rect.
     */
    CGRect rect = [printFormatter rectForPageAtIndex:pageIndex];
    
    /*
     Use a bezier path to draw the borders.
     We may potentially choose not to draw either the top or bottom line
     of the border depending on whether our recipe extended from the previous
     page, or carries onto the subsequent page.
     */
    UIBezierPath *border = [UIBezierPath bezierPath];
    if (pageIndex == printFormatter.startPage) {
        
        // For border drawing, get the rect that includes the formatter area plus the header area.
        // Move the formatter's rect up the size of the custom drawn recipe presentation
        //  and essentially grow the rect's height by this amount.
        rect.origin.y -= self.recipeInfoHeight;
        rect.size.height += self.recipeInfoHeight;
        
        [border moveToPoint:rect.origin];
        [border addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect))];
        
        Recipe *recipe = [formatterToRecipeMap objectForKey:printFormatter];
        
        // Run custom code to draw upper portion of the recipe presentation (image, title, desc)
        [self drawRecipe:recipe inRect:rect];
    }
    
    // Draw the left border
    [border moveToPoint:rect.origin];
    [border addLineToPoint:CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect))];
    
    // Draw the right border
    [border moveToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect))];
    [border addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect))];
    
    if (pageIndex == printFormatter.startPage + printFormatter.pageCount - 1) {
        [border addLineToPoint:CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect))];
    }
    
    /*
     Set the UIColor to be used by the current graphics context, and then stroke 
     stroke the current path that is defined by the border bezier path.
     */
    [[UIColor blackColor] set];
    [border stroke];
}
 
/*
 Custom code to draw upper portion of the recipe presentation (image, title, desc).
 The argument rect is the full size of the recipe presentation.
 */
- (void)drawRecipe:(Recipe *)recipe inRect:(CGRect)rect {
    [self drawRecipeImage:recipe.image inRect:rect];
    [self drawRecipeName:recipe.name inRect:rect];
    [self drawRecipeInfo:recipe.aggregatedInfo inRect:rect];
}
 
- (void)drawRecipeImage:(UIImage *)image inRect:(CGRect)rect {
    
    // Create a new rect based on the size of the header area
    CGRect imageRect = CGRectZero;
    
    // Scale the image to fit in the infoRect
    CGFloat maxImageDimension = self.recipeInfoHeight - PADDING*2;
    CGFloat largestImageDimension = MAX(image.size.width, image.size.height);
    CGFloat scale = maxImageDimension / largestImageDimension;    
    imageRect.size.width = image.size.width * scale;
    imageRect.size.height = image.size.height * scale;
    
    // Place the image rect at the x,y defined by the argument rect
    imageRect.origin = CGPointMake(CGRectGetMinX(rect) + PADDING, CGRectGetMinY(rect) + PADDING);
 
    // Ask the image to draw in the image rect
    [image drawInRect:imageRect];
}
 
// Custom drawing code to put the recipe name in the title section of the recipe presentation's header
- (void)drawRecipeName:(NSString *)name inRect:(CGRect)rect {
    CGRect nameRect = CGRectZero;
    nameRect.origin.x = CGRectGetMinX(rect) + self.recipeInfoHeight;
    nameRect.origin.y = CGRectGetMinY(rect) + PADDING;
    nameRect.size.width = CGRectGetWidth(rect) - self.recipeInfoHeight;
    nameRect.size.height = self.recipeInfoHeight;
    
    [name drawInRect:nameRect withAttributes:nil];
}
 
// Custom drawing code to put the recipe recipe description, and prep time 
// in the title section of the recipe presentation's header
- (void)drawRecipeInfo:(NSString *)info inRect:(CGRect)rect {
    CGRect infoRect = CGRectZero;
    infoRect.origin.x = CGRectGetMinX(rect) + self.recipeInfoHeight;
    infoRect.origin.y = CGRectGetMinY(rect) + TITLE_SIZE*2;
    infoRect.size.width = CGRectGetWidth(rect) - self.recipeInfoHeight;
    infoRect.size.height = self.recipeInfoHeight - TITLE_SIZE*2;
    
    [[UIColor darkGrayColor] set];
    [info drawInRect:infoRect withAttributes:nil];
}
 
@end