Development

Development

Creating a Multipage PDF Document from UIViews in iOS

We created an educational children’s app for iPad that includes a photo scrapbook. Students earn stickers and animal photos for the scrapbook as they use the app, and they are given a few minutes to interact with the scrapbook at the end of each learning session. Since the scrapbook serves as both an indicator of student progress and a fun reward for student effort, we provide an in-app mechanism to export the scrapbook to PDF format so that students will have something tangible to take away from their time with the app (in addition, of course, to increased knowledge and understanding!). In this post, we’ll explore the steps necessary to take a set of UIViews, each representing an individual scrapbook page, and create a multipage PDF document that can be emailed or printed directly from iOS.

Scrapbook Overview

A typical scrapbook contains many pages, each displayed side by side with another to look like a physical book. Here’s an example of two facing pages with photos and stickers, as they appear to students in the app:

Scrapbook facing pages view in app.

Each page is represented by a class called ScrapbookPage, and contains one or two photos and an arbitrary number of stickers (students are free to move stickers around in the scrapbook). We present two ScrapbookPages side by side using a UIPageViewController, which provides really nice page turning interactivity.

We want our PDF output to accurately represent the in-app scrapbook, so we will show two ScrapbookPages side by side on each page of the PDF output. This means that the PDF document needs to be in landscape orientation. We also need to accommodate the possibility of an odd number of ScrapbookPages, in which case the last page of our PDF output will display a single ScrapbookPage rather than two.

Generating PDF Data from a UIView Subclass

Before we tackle the problem of making a multipage document from many ScrapbookPages, let’s start with the more basic task of turning a single UIView subclass into PDF data. In a later section we’ll expand on the basic task by adding logic to loop through an array of ScrapbookPages and create facing page views.

In this basic example, we’ll create a UIView instance that takes up the full screen in landscape orientation, with size 1024x768:

UIView* testView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f)];

Next we create a mutable data object to hold our PDF-formatted output data:

NSMutableData* pdfData = [NSMutableData data];

Then we create a PDF-based graphics context, with our mutable data object as the target:

UIGraphicsBeginPDFContextToData(pdfData, CGRectMake(0.0f, 0.0f, 792.0f, 612.0f), nil);

Note that 792x612 is the size in pixels of a standard 8.5x11” page at 72dpi, in landscape mode. We are passing nil as the last parameter, which could instead be an NSDictionary with additional info for the generated PDF output, such as author name.

Then we mark the beginning of a new page in the PDF output and get the CGContextRef for our PDF drawing:

UIGraphicsBeginPDFPage();
CGContextRef pdfContext = UIGraphicsGetCurrentContext();

Remember that our UIView has size 1024x768, and our PDF page has size 792x612. To make sure that all of the UIView is visible in the PDF output, we must scale the context appropriately. 792 / 1023 = 0.733, which is our scaling factor:

CGContextScaleCTM(pdfContext, 0.773f, 0.773f);

Now that all setup is done, we finally get to the exciting part: rendering the UIView’s layer into the PDF context:

[testView.layer renderInContext:pdfContext];

To finish up, we end the PDF context:

UIGraphicsEndPDFContext();

At this point, we have a an NSData object (pdfData) that contains a PDF representation of our UIView (testView). Here’s all the code from this example together:

UIView* testView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f)];
NSMutableData* pdfData = [NSMutableData data];
UIGraphicsBeginPDFContextToData(pdfData, CGRectMake(0.0f, 0.0f, 792.0f, 612.0f), nil);
UIGraphicsBeginPDFPage();
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
CGContextScaleCTM(pdfContext, 0.773f, 0.773f);
[testView.layer renderInContext:pdfContext];
UIGraphicsEndPDFContext();

Creating a Single Full-screen UIView Subclass Instance from Two ScrapbookPages

In the previous section, we learned how to render a full-screen UIView into a PDF context. We omitted something important, however: the UIView was empty, with nothing in it. That will make for a pretty boring PDF. In this section we’ll see how to add two facing ScrapbookPages to a UIView subclass.

In the app, each ScrapbookPage displayed onscreen has a size of 475x577. Two of these fit side by side on a full-screen landscape page with space between them and a border around the outside, like so:

As mentioned previously, the scrapbook facing pages view in the app is controlled by a UIPageViewController. This provides a very polished and natural simulation of an actual book, with realistic page turning animations as you drag your finger over the pages. This is great for interactive use of the scrapbook, but it’s not necessary for rendering of static pages, and in fact would probably add a lot of CPU and memory overhead to the process. Instead of using a UIPageViewController for the PDF rendering process, we created a simple UIView subclass called ScrapbookOpposingPagesPrintingView. This class manages layout of two ScrapbookPages, on top of a background UIImageView representing an open book.

How do the individual ScrapbookPages get laid out? Before we proceed, we need to introduce another class: ScrapbookPagePrintingView. This UIView subclass takes in its init method a ScrapbookPage object, which is a simple NSObject subclass describing the photos and stickers on a page, and does the actual layout of the photos and stickers described in the ScrapbookPage object by creating UIImageViews for each photo and sticker. The ScrapbookPagePrintingView adds these UIImageViews as subviews to itself. We will not describe this class further, as its internal operations are unimportant to the present discussion.

Here’s what we’ll see in the code below: we have two ScrapbookPage objects, and we create ScrapbookPagePrintingView objects from each one. We add two of these ScrapbookPagePrintingViews to our ScrapbookOpposingPagesPrintingView, which is then ready for rendering to PDF. This diagram shows the relationship between ScrapbookPagePrintingViews and an enclosing ScrapbookOpposingPagesPrintingView:

This is ScrapbookOpposingPagesPrintingView’s interface (.h) file:

@class ScrapbookPage;
@interface ScrapbookOpposingPagesPrintingView : UIView
- (void)showLeftScrapbookPage:(ScrapbookPage*)leftPage rightScrapbookPage:(ScrapbookPage*)rightPage;
@end

And here is ScrapbookOpposingPagesPrintingView’s implementation (.m) file:

#import "ScrapbookOpposingPagesPrintingView.h"
#import "ScrapbookPagePrintingView.h"
#import "ScrapbookPage.h"

static const CGRect ScrapbookOpposingPagesLeftPageFrame = {36.0f, 92.0f, 475.0f, 577.0f};
static const CGRect ScrapbookOpposingPagesRightPageFrame = {514.0f, 92.0f, 475.0f, 577.0f};

@implementation ScrapbookOpposingPagesPrintingView 

- (id)init {
    CGRect fullScreenFrame = CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f);
    self = [super initWithFrame:fullScreenFrame];
    if (self) {
        self.backgroundColor = [UIColor whiteColor];
        UIImageView* backgroundImageView = [[UIImageView alloc] initWithFrame:myFrame];
        [backgroundImageView setBackgroundColor:[UIColor clearColor]];
        [backgroundImageView setContentMode:UIViewContentModeCenter];
        [backgroundImageView setImage:
          [UIImage imageNamed:@"scrapbook-print-bg"]];
        [self addSubview:backgroundImageView];
    }
    return self;
}

- (void)showLeftScrapbookPage:(ScrapbookPage*)leftPage rightScrapbookPage:(ScrapbookPage*)rightPage {
    if (leftPage != nil) {
        ScrapbookPagePrintingView* leftPageView =
          [[ScrapbookPagePrintingView alloc]
          initWithFrame:ScrapbookOpposingPagesLeftPageFrame
          andScrapbookPage:leftPage];
        [self addSubview:leftPageView];
    }
	
    if (rightPage != nil) {
        ScrapbookPagePrintingView* rightPageView =
          [[ScrapbookPagePrintingView alloc]
          initWithFrame:ScrapbookOpposingPagesRightPageFrame
          andScrapbookPage:rightPage];
        [self addSubview:rightPageView];
    }
}

In its init method, we call [super initWithFrame:] and pass a full-screen landscape orientation frame. Then we add a UIImageView containing the image representing an open book, which will be behind the two facing pages:

In the showLeftScrapbookPage:rightScrapbookPage: method, we accept one or two ScrapbookPage objects and create ScrapbookPagePrintingViews from them, then add them as subviews to self. Note that we pass different statically-defined CGRect frames to the ScrapbookPagePrintingView init method for left and right pages, to make sure that the resulting ScrapbookPagePrintingViews show up on the left or right side when added as subviews to self. These frames have different origins for left and right, but the same size, since each ScrapbookPagePrintingView is the same size.

Instantiating a ScrapbookOpposingPagesPrintingView and passing one or two ScrapbookPages to showLeftScrapbookPage:rightScrapbookPage: results in a full-screen landscape orientation ScrapbookOpposingPagesPrintingView with an open book background image and two ScrapbookPages:

Putting it All Together

Now that we know how to generate PDF data from a single UIView or UIView subclass, and we know how to create a ScrapbookOpposingPagesPrintingView class containing two facing ScrapbookPages, we will add logic to iterate over an array of ScrapbookPages to create as many ScrapbookOpposingPagesPrintingViews as we need, noting that the last ScrapbookOpposingPagesPrintingView may only have a single ScrapbookPage on it if we have an odd number of ScrapbookPages.

To accomplish this, we need two methods: one that prepares the pdfData mutable data object to hold the PDF output and iterates through the ScrapbookPages, and one that creates and renders a ScrapbookOpposingPagesPrintingView for each pair of pages. Here’s the first method:

- (NSData*)scrapbookPdfDataForScrapbookPages:(NSArray*)scrapbookPages {
    NSMutableData* pdfData = [NSMutableData data];
    UIGraphicsBeginPDFContextToData(pdfData, CGRectMake(0.0f, 0.0f, 792.0f, 612.0f), nil);
	
    if (scrapbookPages.count > 0) {
        NSUInteger pageIndex = 0;
        do {
            ScrapbookOpposingPagesPrintingView* printingView =
              [[ScrapbookOpposingPagesPrintingView alloc] init];

            ScrapbookPage* leftPage = scrapbookPages[pageIndex];
            // only include right page if it exists
            ScrapbookPage* rightPage =
              pageIndex + 1 < scrapbookPages.count ?
              scrapbookPages[pageIndex + 1] :
              nil;
            [printingView
              showLeftScrapbookPage:leftPage
              rightScrapbookPage:rightPage];
            [self addPrintingViewPDF:printingView];
            // take two pages at a time
            pageIndex += 2;
        }
        while (pageIndex < scrapbookPages.count);
    }
	
    UIGraphicsEndPDFContext();		
	
    return pdfData;
} 

And the method that performs the rendering to PDF for each ScrapbookOpposingPagesPrintingView, called by the method above, is:

- (void)addPrintingViewPDF:(UIView*)printingView {
    // Mark the beginning of a new page.
    UIGraphicsBeginPDFPage();
    CGContextRef pdfContext = UIGraphicsGetCurrentContext();
	
    // Scale down from 1024x768 to fit paper output (792x612; 792/1024 = 0.773)
    CGContextScaleCTM(pdfContext, 0.773f, 0.773f);
    [printingView.layer renderInContext:pdfContext];
}

Calling scrapbookPdfDataForScrapbookPages: with an array of ScrapbookPages results in an NSData object containing a PDF representation of the entire scrapbook, which can be used in many ways. In the app, we enable printing of the PDF output directly from the app via AirPrint, and also emailing it as a file attachment. Perhaps we’ll cover those two mechanisms in another blog post.

comments powered by Disqus