iOS 5에서 Quartz 2D를 사용하여 PDF를 만들기 – 파트 1

Tope Abayomi Tope Abayomi

이 포스트는 중국어 간체, 영어 언어로도 제공됩니다.

iOS 개발자이고 iPhone App Design의 창업자이며 iOS 튜토리얼 팀 멤버인 Tope Abayomi이 작성한 블로그 포스트 입니다.

Learn how to create a PDF programmatically!

Learn how to create a PDF programmatically!

당신은 가끔 당신의 앱에서 당신이 유저들을 위해 PDF를 생성하고 싶을 때가 있을 것입니다. 예를 들자면, 유저가 계약서에 싸인하는 앱을 상상해보세요 – 당신은 최종 결과물로 PDF를 얻기를 원할 것입니다.
어떻게 프로그램으로 PDF를 만들까요? iOS에서 Quartz2D의 도움을 받으면 쉽습니다!
이 튜토리얼 시리즈에서, Quartz를 간단한 PDF를 생성하는 경험하게 될 것입니다. 우리는 스크린 샷처럼 PDF로 ‘청구서’를 만들 것입니다.
이 튜토리얼은 당신이 스토리보드나 ARC같은 iOS 5의 새 기능들에 익숙할 것이라고 간주합니다. 만일 iOS5를 경험해 보지 않았으면 iOS 5 튜토리얼을 먼저 확인 하시기 바랍니다.

시작!

Xcode를 실행하고 “iOSApplicationSingle View Application template” 프로젝트를 생성하세요. 프로젝트에 “PDFRenderer”라는 이름을 주세요. “Device Family”는 “iPhone”으로 고르고, “Use Storyboard”와 “Use Automatic Reference Counting”을 체크하시고, 프로젝트 생성을 완료하면 됩니다.

이 프로젝트에서 우리는 2개의 화면을 사용할 것입니다. 하나는 버튼을 탭하면 PDF를 보여주고, 하나는 PDF 자체입니다.
“MainStroyboard.storyboard” 파일을 선택하면 메인 창에서 뷰 컨트롤러가 보입니다. 이 뷰 컨트롤러는 네비게이션 컨트롤러에  넣어야 합니다. 그러려면 Editor 메뉴에서 “Embed In/Navigation Controller”를 클릭하세요.

이 뷰-컨트롤러는 네비게이션-컨트롤러로부터 오는 세구에를 가지고 있습니다.

“Object”탭에서 UIButton을 드래그하고 “Draw PDF”로 이름을 주세요.

앱을 실행하면 버튼 하나를 가진 간단한 뷰가 보입니다. 하지만 버튼을 눌러도 아무것도 하지 않습니다. 잠깐 더 진행해 보겠습니다.
이제 PDF를 홀드할 두번째 뷰를 추가하겠습니다.
“Object”탭에서 새로운 뷰 컨트롤러를 스토리보드위에 드래그 하세요. “Draw PDF”를 새로운 뷰 컨트롤러에 Ctrl+Drag를 하세요. 마우스 버튼을 놓으면 아래 그림처 보이게 됩니다.

“Push”옵션을 선택하세요. 버튼을 탭하면 새 뷰 컨트롤러로 이어지는 세구에가 나타납니다. (다른 표현으로) 이제 버튼은 기능을 가졌습니다!
앱을 실행하고, 버튼을 누르면, 빈 뷰 컨트롤러가 푸쉬되는 것이 보입니다. 스토리보트 룰입니다!

PDF를 만들고 테스트를 그리자

이제 우리 PDF를 위한 프레임워크가 있고, 코드를 작성할 준비가 되었습니다.
그전에, 메뉴에서 “FileNewNew File”을 선택하여 프로젝트에 새 파일을 하나 추가해야 합니다. “iOSCocoa TouchObjective-C class”를 선택하고, “Next” 버튼을 탭하고, “Class”에 “PDFViewController”라는 이름을 주고, “Subclass of”에는 “UIViewController”를 고르고, “With XIB for user interface”는 체크되지 않도록 확인 하세요. 다시 “Next”버튼을 를 누르고, “Create” 버튼을 눌러서 파일을 생성하세요. 우리는 nib파일이 필요 없습니다. 왜냐면 뷰 컨트롤러를 스토리보드에 생성했기 때문입니다.
나중의 뷰 컨트롤러를 새로 만든 파일에 연결하겠습니다. 스토리보드에서 나중의 뷰 컨트롤러를 선택하고 “Identity Inspector”에서 클래스를 “PDFViewController”로 바꾸세요.

PDF에 텍스트를 그리려면 “Core Text framework”가 필요합니다. “Project navigator”탭에서 “PDFRenderer” target을 선택하고, “Build Phases”탭에서 “Link Binaries With Libraries” 스트립을 펼치고, “+”를 눌러서 “CoreText.framework”를 선택하고 “Add” 버튼을 누르세요.

PDFViewController.h를 열고 CoreText를 import하세요.

#import <CoreText/CoreText.h>;

PDFViewController.m가 “hello world” PDF를 생성하도록 아래 메소드를 추가하세요. 긴 메소드이지만 걱정마세요. 잠시후에 잘게 잘게 설명할테니까요.

-(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;
 
    // 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);
 
    // 렌더링 할 프레임을 얻습니다.
    CFRange currentRange = CFRangeMake(0, 0);
    CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
    CGPathRelease(framePath);
 
    // 612 x 792 기본 크기를 가지는 PDF context를 생성합니다.
    UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);
 
    // 새 페이지 시작을 마킹합니다.
    UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
 
    // graphics context를 얻습니다.
    CGContextRef currentContext = UIGraphicsGetCurrentContext();
 
    // 텍스트 행렬을 확실한 기본 행렬로 설정합니다. 기존 스케일 비율은 사용되지 않습니다.
    CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
 
    // Core Text는 좌측하단이 코너가 기준점입니다. 그리기 전에 현재 transform을 상하가 바뀌도록 하겠습니다. (역주: 행열연산)
    CGContextTranslateCTM(currentContext, 0, 100);
    CGContextScaleCTM(currentContext, 1.0, -1.0);
 
    // 프레임을 그립니다.
    CTFrameDraw(frameRef, currentContext);
 
    CFRelease(frameRef);
    CFRelease(stringRef);
    CFRelease(framesetter);
 
    // PDF context를 닫고 컨텐츠를 출력합니다.
    UIGraphicsEndPDFContext();
}

첫 6개 라인은 Documents 폴더에  PDF를 생성하도록 파일 이름을 지정합니다. 이 파일 이름은 “Invoice.pdf”로 하였습니다.

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

다음 블록은 PDF에 그릴 “Hello world” 문자열을 생성합니다. 이 문자열은 CoreGraphics에 대응하는 CFStringRef 타입으로 변환해야 합니다.

NSString* textToDraw = @"Hello World";
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Core Text Framesetter를 사용 준비.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);

이제 텍스트를 출력할 프레임을 정의하는 CGRect를 생성합니다.

CGRect frameRect = CGRectMake(0, 0, 300, 50);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
// 렌더링할 프레임을 구합니다.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);

다음은 PDF context(콘텍스트;문맥)를 생성하고, PDF 페이지 시작을 마킹합니다. 매 PDF 페이지의 시작은 UIGraphicsBeginPDFPageWithInfo를 호출하면 됩니다.

// 612 x 792 기본 크기를 가지는 PDF context를 생성합니다.
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);
// 새 페이지 시작을 마킹합니다.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
// graphics context를 얻습니다.
CGContextRef currentContext = UIGraphicsGetCurrentContext();

Core Graphics의 그리기 좌표는 좌측 하단 코너에서 시작되고, UIKit 전역 좌표는 좌측 상단 코너에서 시작됩니다. 그래서 그리기 전에 context를 뒤집어야 합니다.

// 텍스트 행렬을 확실한 기본 행렬로 설정합니다. 기존 스케일 비율은 사용되지 않습니다.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
// Core Text는 좌측하단이 코너가 기준점입니다. 그리기 전에 현재 transform을 상하가 바뀌도록 하겠습니다. (역주: 행열연산)
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);

그리고 텍스트를 가지고 있는 프레임을 그립니다. 모든 Core Graphics 오브젝트를 해제하고, PDF context를 닫습니다. (고로 디스크에 파일을 저장합니다.).

// 프레임을 그립니다.
CTFrameDraw(frameRef, currentContext);
CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
// PDF context를 닫고 컨텐츠를 출력합니다.
UIGraphicsEndPDFContext();

만일 Core Text가 어떻게 동작하는지 더 알고 싶으면 우리가 준비해 둔  Core Text를 보시길 권합니다.

UIWebView를 추가하여 PDF 파일을 보자.

화면에 PDF 파일을 보여주는 작업만 남았습니다. 그렇게 하려면 PDFViewController.m에 아래의 메소드를 추가하세요, 이것은 뷰 컨트롤러에  UIWebView를 추가하여 우리가 생성한 PDF 파일 경로를 보여줍니다.

-(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];    
}

그러고나서 viewDidLoad가 새로운 메소드를 호출하도록 구현하세요.

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

이제 우리는 결과를 볼 수 있습니다! 프로젝트를 빌드하고 실행하면, 화면상에서 ” Hello World”를 볼 수 있고 확대도 됩니다.

퀵 리팩토링 하기

PDF를 그리는 코드는 뷰 컨트롤에 속해 있지 않습니다. 그래서 그 코드를 PDFRenderer라고 이름 붙인 새 NSObject에 이식하려고 합니다. “iOSCocoa TouchObjective-C class” 템플릿을 사용하여 새 파일을 생성하세요. “Subclass of”에는 NSObject를 선택하고, “Class”에는 “PDFRenderer”라고 입력하고, Next 버튼과 Create 버튼을 눌러 파일을 생성하고,  새로 생성된 “PDFRenderer.h”을 오픈하세요. 그리고 가장 위에 “Core Text”를 import하세요:

#import CoreText/CoreText.h

PDFViewController.m에서 PDFRenderer.m으로 drawText를 옮기세요.
우리는 drawText 메소드에 파일이름을 전달할 계획입니다. 그래서 PDFViewController.m 파일에 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;
}

다음은 PDFRenderer.m를 열고 drawText메소드에서 같은 블럭을 제거하고, 파일 이름을 파라미터로 전달 할 수 있도록 메소드 특성을 변경하고 그것을 클래스 메소드로 만드세요: (역주: 원문은 static method – C++ 용어)

+(void)drawText:(NSString*)pdfFileName

이 메소드는 PDFRenderer.h에도 선언을 해야 합니다.
다음은, PDFViewController.m의 처음 부분에서 PDFRenderer를 import합니다:

#import "PDFRenderer.h"

그리고 viewDidLoad를 변경하여 이 새 클래스 메소드를 호출하세요:

- (void)viewDidLoad
{
    NSString* fileName = [self getPDFFileName];
 
    [PDFRenderer drawText:fileName];
    [self showPDFFile];
 
    [super viewDidLoad];
}

프로젝트를 빌드하고 실행해 보세요. 우리의 퀵 리팩토링 결과는 앱에 바뀐게 없군요. 하지만 훨씬 좋은 구조죠.

Quartz 2D를 사용하여 라인을 그리자.

우리는 청구서가 텍스트, 라인, 그리고 이미지들로 만들어지기를 원합니다. 우리는 텍스트로 해 보았습니다. 이제 라인을 그리기를 연습 하겠습니다. 그렇게 하려면 뭔가 필요할까요? 바로! drawLine 메소드가 필요합니다.
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);
}

위 코드는 우리가 그리려는 라인의 속성을 설정합니다. 그 속성들은 라인의 굵기(2.0)과 색상(반투명한 회색)입니다. 그리고 메소드에 전달한 CGPoint 사이에 라인을 그립니다.
이제 우리는 뷰 컨트롤러에서 이 메소드를 호출 할 수 있습니다. 그런데 drawText 메소드는 PDF Graphic context를 생성하지 않고 UIGraphicsBeginPDFContextToFile를 호출하여 새 페이지를 만듭니다. 그래서 몇가지 수정해야 합니다.
먼저, PDFRenderer 파일에 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();
}

이 코드는 새 graphics context를 생성하여, 텍스트와 라인을 그립니다. 그리고 context를 종료하죠.
drawText 메소드는 더 이상 PDF filename을 필요로 하지 않습니다. 아래에 새 drawText 메소드 코드 입니다.

+(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);
}

PDFRenderer.h에 drawPDF 메소드 선언을 추가 하세요. 그리고 나서 뷰 컨트롤러의 viewDidLoad에서 올바른 메소드 drawPDF (drawText)를 호출하도록 수정하세요.
이제 되었습니다. 빌드하고 실행해 보세요. “Hello World”와 대각선을 볼 수 있습니다. 아래에 이미지와 비슷할 겝니다.

우스꽝스러워 보이죠? 하지만 이건 “Quartz 2D를 사용하여 추상화 그리기”가 아니랍니다. 걱정마세요.  이 튜터리얼의 파트2에서 더 멋지게 다음겠습니다! :]

앞으로 어떻게 진행합니까?

위 튜토리얼의 모든 코드를 담은 예제 프로젝트 파일이 있습니다.
이제 이 튜토리얼의 첫 파트를 마칩니니다. 파트2, 에서 우리는 이미지를 추가하고 xib 파일을 통해 레이아웃을 쉽게 만드는 등 더 향상된 그리기 기술을 진행할 것입니다. 질문이나 코멘트가 있으면 멀리 갈 필요 없이 아래 포럼 논의에 가입하여 주세요!
 사용하기 쉽고, 유용하고, 심미적으로 유쾌한 앱에 열정이 넘치는, iOS 튜토리얼 팀 멤버인 Tope Abayomi이 작성한 블로그 포스트 입니다.   여기에 커스텀 디자인을 통해  앱을 디지인 하능 방법을 가르쳐 주는 몇개의 비디오가 있습니다.

Tope Abayomi
Tope Abayomi

Tope Abayomi is an iOS developer and Founder of App Design Vault, your source for iPhone App Design. You can find Tope on Twitter.

User Comments

0 Comment

Other Items of Interest

Ray의 월간 뉴스레터

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Dani Arnaout
  • Jack

... 49 total!

Update Team

Editorial Team

  • Matt Galloway

... 23 total!

Code Team

  • Orta Therox

... 3 total!

번역 팀

  • Team Tyran

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!