2012年1月4日星期三

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 1

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 1:
This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer and Founder of App Design Vault, your source for iPhone App Design.

Learn how to create a PDF programmatically!
Learn how to create a PDF programmatically!

Sometimes in your apps you might want to generate a PDF with data from the app for your users. For example, imagine you had an app that allowed users to sign a contract – you would want the users to be able to get a PDF with the final result.

But how do you generate a PDF programmatically? Well, it’s easy to do in iOS with the help of Quartz2D!

In this tutorial series, you’ll get hands-on experience with creating a simple PDF with Quartz2D. The PDF we’ll make will be for an invoice-making app, as you can see in the screenshot.

This tutorial assumes you are familiar with the basic new features in iOS 5 such as Storyboards and ARC. If you are new to iOS 5, check out some of the other iOS 5 tutorials on this site first.



Getting Started


Run Xcode and create a new project with the iOS\Application\Single View Application template. Enter PDFRenderer for the project name, choose iPhone for the Device Family, make sure Use Storyboard and Use Automatic Reference Counting are checked, and finish creating the project.



We are going to use two screens in this project. The first will simply have a button that will show the PDF when tapped. The second screen will be the PDF itself.

Select the MainStoryboard.storyboard file. In the main window, you will see a View Controller. We need this View Controller to be embedded in a Navigation Controller to start with, so click on the Editor Menu, then select Embed In/Navigation Controller.



The View Controller now has a segue from the Navigation Controller.



Now drag a UIButton from the objects tab to the View Controller, and rename the label “Draw PDF.”



If you run the application, you should see a simple View with a button displayed that says “Draw PDF,” but does nothing when tapped. We’ll take care of that shortly.

Now let’s add the second View that will hold the PDF.

Drag a new View Controller from the objects tab onto the Storyboard. Ctrl+Drag the “Draw PDF” button onto the new View Controller. When you release the mouse, you should see a popup similar to the one in the image below.



Select the Push option. This will create a segue onto the new View Controller so that it is displayed when the button is tapped. In other words, our button is now functional!

Run the application, tap the button, and you should see an empty View Controller pushed onto the screen. Storyboards rule!

Creating the PDF and Drawing Text


Now that we have the framework for our PDF, we’re ready to write some code.

Before we do that, select File\New\New File to add a new file to the project. Choose the iOS\Cocoa Touch\UIViewController subclass template, enter PDFViewController for the Class and UIViewController for the Subclass, make sure “With XIB for user interface” is NOT checked, and finish creating the file. We do not need a nib because we will use the View Controller created in the storyboard.

Connect the last View Controller we created to this new file by selecting the View Controller on the Storyboard and changing the class to PDFViewController in the identity inspector.



To draw some text in our PDF, we’re going to need to use the Core Text framework. To do this, select the PDFRenderer target and go to the Build Phases tab. Click the + sign below the Link Binaries With Libraries option, and then select the CoreText framework.



Then open PDFViewController.h and import the CoreText header:


#import <CoreText/CoreText.h>


OK time for the code! Add a new method to PDFViewController.m to create a “hello world” PDF. This is a long method, but don’t worry – we’ll explain it bit by bit afterwards.


-(void)drawText
{
NSString* fileName = @"Invoice.PDF";

NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];

NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;

// Prepare the text using a Core Text Framesetter.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);

CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);

// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);

// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);

// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();

// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);

// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);

// Draw the frame.
CTFrameDraw(frameRef, currentContext);

CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();

}


The first six lines create a PDF filename for a file that will reside in the Documents folder. The file will be called Invoice.pdf.


NSString* fileName = @"Invoice.PDF";

NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];


The next block of code creates a “Hello world” string that we will draw onto the PDF. It also converts the string to its CoreGraphics counterpart, CFStringRef.


NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;

// Prepare the text using a Core Text Framesetter.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);


Now we create a CGRect that defines the frame where the text will be drawn.


CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);

// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);


Next we create a PDF context and mark the beginning of a PDF page. Each page of the PDF has to start with a call to UIGraphicsBeginPDFPageWithInfo.


// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);

// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();


The coordinates of Core Graphics drawings start from the bottom-left corner, while UIKit global coordinates start from the top-left. We need to flip the context before we begin drawing.


// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);

// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);


Then we draw the actual frame with the text, release all the Core Graphics objects, and close the PDF context (hence writing the PDF to disk).


// Draw the frame.
CTFrameDraw(frameRef, currentContext);

CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();


If you are interested in learning more about how Core Text works and some more cool things you can do with it, check out our Core Text tutorial.

Add a UIWebView to Show the PDF File


The only thing left to do is to show our PDF file on the screen. To do that, add the following method to PDFViewController.m, which adds a UIWebView to the View Controller and shows the PDF file path we just created.


-(void)showPDFFile
{
NSString* fileName = @"Invoice.PDF";

NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];

UIWebView* webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];

NSURL *url = [NSURL fileURLWithPath:pdfFileName];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView setScalesPageToFit:YES];
[webView loadRequest:request];

[self.view addSubview:webView];   
}


Then add the implementation of viewDidLoad to call these new methods:


- (void)viewDidLoad
{
[self drawText];
[self showPDFFile];

[super viewDidLoad];
}


Now we’re ready to see some results! Build and run the project, and you should see “Hello World” on the screen when you zoom in!



A Quick Refactoring Process


Our drawing code does not really belong in the View Controller, so let’s farm that off into a new NSObject called PDFRenderer. Create a new file with the iOS\Cocoa Touch\Objective-C class template, enter PDFRenderer for the Class and NSObject for the subclass, and finish creating the file.

Open up PDFRenderer.h and import Core Text at the top of the file:


#import


Then move the drawText method from PDFViewController.m to PDFRenderer.m.

We will pass the filename into the new drawText method, so let’s create a new method in the PDFViewController.m file called getPDFFileName.


-(NSString*)getPDFFileName
{
NSString* fileName = @"Invoice.PDF";

NSArray *arrayPaths =
NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory,
NSUserDomainMask,
YES);
NSString *path = [arrayPaths objectAtIndex:0];
NSString* pdfFileName = [path stringByAppendingPathComponent:fileName];

return pdfFileName;

}


Next, open PDFRenderer.m and remove this same block of code from the drawText method, and modify the method signature to take the filename as a parameter and make it a static method:


+(void)drawText:(NSString*)pdfFileName


Also predeclare this method in PDFRenderer.h.

Next, import PDFRenderer at the top of PDFViewController.m:


#import "PDFRenderer.h"


And modify viewDidLoad to call this new class and method:


- (void)viewDidLoad
{
NSString* fileName = [self getPDFFileName];

[PDFRenderer drawText:fileName];
[self showPDFFile];

[super viewDidLoad];
}


Build and run the project. Our quick refactoring shouldn’t have caused the application to behave any differently, but the code is better organized.

Drawing a Line Using Quartz 2D


The invoice we want to end up with is made up of text, lines and images. We’ve got text — now it’s time to practice drawing a line. To do that, we will use… wait for it… the drawLine method!

Add this new method to PDFRenderer.m:


+(void)drawLineFromPoint:(CGPoint)from toPoint:(CGPoint)to
{
CGContextRef context = UIGraphicsGetCurrentContext();

CGContextSetLineWidth(context, 2.0);

CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();

CGFloat components[] = {0.2, 0.2, 0.2, 0.3};

CGColorRef color = CGColorCreate(colorspace, components);

CGContextSetStrokeColorWithColor(context, color);


CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);

CGContextStrokePath(context);
CGColorSpaceRelease(colorspace);
CGColorRelease(color);

}


The above code sets the properties of the line we want to draw. The properties are the thickness of the line (2.0) and the color (transparent gray). It then draws the line between the CGPoints passed into the method.

We could now call this method from our View Controller. Notice, however, that the drawText method does not create a new PDF Graphics context or a new page by calling UIGraphicsBeginPDFContextToFile. So we need to make some modifications.

First, create a new method in the PDFRenderer file called drawPDF.


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

CGPoint from = CGPointMake(0, 0);
CGPoint to = CGPointMake(200, 300);
[PDFRenderer drawLineFromPoint:from toPoint:to];

[self drawText];

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}


This will create our graphics context, draw the text and a test line, and then end the context.

Note that the drawText method no longer takes the PDF filename as a parameter. Here is our new drawText method.


+(void)drawText
{

NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Prepare the text using a Core Text Framesetter
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);

CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);

// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);

// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();

// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);

// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);

// Draw the frame.
CTFrameDraw(frameRef, currentContext);

CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
}


Add the predeclaration for drawPDF into PDFRenderer.h. Then modify the View Controller to call the correct method in viewDidLoad, which is drawPDF instead of drawText.

That’s it! Build and run the app, you should see both “Hello World” and a diagonal line, similar to the image below.



Yes, that’s funny-looking. And no, this is not “How to Draw Abstract Art with Quartz 2D.” Don’t worry, our PDF will achieve some polish in Part Two of the tutorial! :]

Where to Go From Here?


Here is a example project with all of the code from the above tutorial.

That brings us to the end of the first part of this tutorial. In Part Two, we will go into more advanced drawing techniques, like adding images and using a xib file to make the layout process easier. Please stay tuned!

If you have any questions or comments about what we’ve done so far, or if there is anything you’d like me to address in Part Two, please join in the forum discussion below!



This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer with a passion for easy to use, useful, aesthetically pleasing apps. Here are some of his videos teaching you how to design an app with custom design.

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 1 is a post from: Ray Wenderlich

没有评论:

发表评论