iOS绘图和打印编程指导(六)-iOS打印

在iOS 4.2和以后,应用程序可以添加对本地打印机的打印内容的支持。尽管并非所有应用程序都需要打印支持,但是如果应用程序用于创建内容(例如文字处理器或绘图程序)、进行购买(打印订单确认)和用户可能合理地希望永久记录的其他任务,那么打印通常是一个有用的特性。

本章说明如何向应用程序添加打印支持。在高级别上,您的应用程序创建打印作业,提供准备打印的图像和PDF文档的数组、单个图像或PDF文档、任何内置打印格式化程序类的实例或自定义页面渲染。

术语说明:印刷作业(print job)的概念在本章中出现了很多次。打印作业是一种工作单元,它不仅包括要打印的内容,还包括打印中使用的信息,例如打印机的身份、打印作业的名称以及打印的质量和方向。

IOS中的打印设计简单直观


为了打印,用户点击通常位于导航栏或工具栏中的按钮,该按钮与用户想要打印的视图或选定项目相关联。然后,应用程序呈现了打印选项的视图。用户选择打印机和各种选项,然后请求打印。应用程序被要求从其内容生成打印输出或提供可打印数据或文件URL。请求的打印作业被假脱机,控件返回到应用程序。如果目标打印机当前不忙,打印立即开始。如果打印机已经在打印,或者如果在队列中它之前有作业,则打印作业将保留在iOS打印队列中,直到它移动到队列的顶部并被打印。

打印界面

用户看到的与打印相关的第一件事是打印按钮。打印按钮通常是导航栏或工具栏上的bar button item。打印按钮应该在逻辑上使用应用程序呈现的内容;如果用户点击按钮,应用程序应该打印该内容。尽管打印按钮可以是任何自定义按钮,但是建议您使用图6-1所示的系统barButtonItem。这是一个UIBarButtonItem对象,用UIBarButtonSystemItemAction常量指定,可以在接口生成器中创建该对象,也可以通过调用initWithBarButtonSystemItem:target:action:方法.

图5-1 系统的print按钮

当用户点击打印按钮时, 应用程序的控制器对象会接受到打印动作消息. 控制器的响应是准备打印和显示打印机选项的视图. 这些选项包括目标打印机(从可发现打印机列表中选择), 副本的数量, 有时还包含要打印的页的范围. 如果所选打印机能够进行双面打印, 用户可以选择单面或双面打印输出. 如果用户选择不打印, 他们点击选项视图范围以外的部分(ipad上), 或者点击cancel按钮(iPhone和iPod touch上)来取消打印机选项视图.

打印选项界面的类型取决于设备. 在iPad上, UIKit框架使用popover视图来显示选项, 如图6-2.


图6-2 iPad中的打印选项视图

在iPhone或者iPod上, UIKit将打印选项界面从底部往上滑出, 如图6-3.


图6-3 iPhone上的打印选项页

一旦你提交了打印作业, 那么打印作业正在进行或者正在打印队列中等待打印, 用户可以通过双击home按钮从而显示多任务UI界面, 选择打印中心(Print Center)来查看其状态. 打印中心(如图6-4)是一个后台系统应用程序, 该程序显示打印队列总的作业顺序, 包括当前正在打印的作业, 该程序只有当前打印作业正在进行时才有用.
图6-4 打印中心

在打印中心, 用户可以点击打印作业来获取该作业的具体信息(如图6-5)也可以取消正在打印或排队中的打印作业.


图6-5 打印中心-打印作业的具体信息

iOS中的打印原理

APP使用UIKit中打印API来收集打印作业的要素, 包括要打印的内容和与打印作业相关的信息. 然后显示打印选项界面. 用户作出选择点击打印按钮. 在某些情况下, UIKit框架要求APP将打印的内容绘制出来, UIKit会将这些绘制内容记录为PDF数据, 然后UIKit会将打印数据提交给打印子系统.

打印系统会做一些事情来完成打印作业. 当UIKit将打印数据传递给打印子系统时, 它将此数据写入存储单元中. 它还捕获有关打印作业的信息, 打印系统在先进先出队列中管理着组合的打印数据和每个打印作业的metadata. 设备上的多个应用可以向打印子系统提交多个打印作业, 所有这些作业都放在打印队列中. 每个设备都有一个队列用于所有打印作业, 不管它来自那个APP或目的打印机.

当打印作业到了队列的顶部时, 系统打印守护进程(这里称之为printd)考虑目标打印机的要求, 并在必要的时候将打印数据转换为打印机识别的数据类型. 打印系统会向用户报告诸如"没纸了"之类的错误警报. 它还以编程方式向打印中心报告打印作业的进程情况, 打印中心先士卒如打印作业的page 2 of 5之类的信息.

图6-6 打印系统的总体架构

UIKit中的打印API


UIKit中print API包括8个类和一个protocol. 这些类的实例和实现协议的delegate在runtime具有的关系如图6-7所示.


图6-7 UIKit打印对象间的关系

打印支持概述

在高层上, 有两个方法可以将打印能力添加到APP中. 如果你正在使用UIActivityViewController, 并且如果你不需要控制用户选择过程的能力, 包括是否可以选择页面范围或覆盖纸张的选择. 那么你可以添加打印活动. 否则, 必须使用类UIPrintInteractionController来向APP中添加打印能力.
当用户告诉APP打印时, 类UIPrintInteractionController的单例可以向你提供APP指定应该做啥的能力, 它包括有关打印作业(UIPrintInfo)和纸张大小和打印内容的区域(UIPrintPaper)的信息. 它还可以引用实现UIPrintInteractionControllerDelegate来进一步设置打印行为的delegate对象.

更重要的是, 打印交互控制器可以让你的APP提供要打印的内容. UIPrintInteractionController类提供了三种不同打印方式:

  • 静态image或PDF文件. 对于简单的内容, 可以使用打印交互控制的printingItemprintingItems属性来提供image(各种格式的), PDF文件或多个image或PDF文件
  • Print formatters. 如果需要自动回流(reflow)打印文本和Html内容, 可以将内置的print formatter的实例赋值给打印交互控制器的printFormatter属性
  • Page renderers. Page renderer可以让你自定义打印内容, 让你完全控制页面布局, 包括页眉和页脚. 要使用page renderer, 必须先编写page renderer的子类, 然后将其实例赋值给打印交互控制器的printPageRenderer属性.

重要: 上面提到四个属性是互斥的, 也就是说, 如果为其中一个属性赋值, UIKit会将其他三个属性的值置为nil.

有了这样一系列的选项,你的应用程序的最佳选择是什么?表6-1阐明了作出这一决定的因素。
6-1 决定如何打印APP内

如果... 就...
APP可以访问可直接打印的内容(图片或PDF) 使用printingItem或者printingItems属性
想打印单个image或PDF并且可以让用户选择页面范围 使用printingItem属性
您希望打印纯文本或HTML文档(并且不希望附加内容, 如页眉和页脚) 将一个UISimpleTextPrintFormatterUIMarkupTextPrintFormatter对象赋值给printFormatter属性, 而且print-formatter对象使用plain或者HTML文本初始化.
你希望打印一个UIKit视图中的内容(并且不需要打印其他附件内容,页面页脚) 从需要打印的视图中获取一个UIViewPrintFormatter对象, 并将其赋值给printFormatter属性.
你希望打印的页面显示页面页脚, 和增加的页号 UIPrintPageRenderer对象赋值给printPageRenderer属性. 你可以将UIPrintPageRenderer的子类赋值给属性, 这样你就可以自定义页面渲染.
您希望对打印的内容有最大程度的控制 UIPrintPageRenderer的子类实例赋值给属性printPageRenderer,并绘制所打印的所有内容。

打印工作流

打印图片, PDF或其他可打印的内容的一般步骤如下:

  1. 获取UIPrintInteractionController单例

  2. (可选, 但强烈建议)创建一个UIPrintInfo对象, 设置一些属性比如, output type, job name, 打印方向; 然后将该对象赋值给打印交互控制器的printInfo属性. (Apple强烈建议你设置output type和job name)
    如果你设置printInfo对象,那么UIKit会给打印作业设置默认的属性(比如, 作业名称(job name)是APP的名称)

  3. (可选)将一个委托controller赋值给delegate属性, 该委托控制器必须实现UIPrintInteractionControllerDelegate协议. 通过这个委托可以做很多事. 委托可以在打印选项呈现和撤销时, 以及打印作业开始和结束时做出适当的响应. 它可让打印交互控制器返回到父视图控制. 另外, 默认情况下, UIKit根据输出类型(output type)表明APP正在打印的内容类型,这样可以让系统选择默认的纸张大小和可打印区域. 如果APP需要对打印纸张的大小进行更多的控制, 则可以重写标准行为. 后续内容会具体讲解.

  4. 对下面UIPrintInteractionController的四个属性之一进行赋值:

    • 将包含PDF或image数据引用的NSData, NSURL, UIImage, ALAsset对象赋值给printingItem属性
    • 如果要打印一组image或者PDF文档,则可以将一组和打印内容相关的NSData, NSURL, UIImage, ALAsset对象赋值给printingItems属性
    • 如果要对打印内容进行自定义布局可以给赋值一个UIPrintFormatter对象给printFormatter属性
    • printPageRenderer赋值一个UIPrintPageRenderer对象.
  5. 如果在前一个步骤中赋值了page renderer对象, 那么该对象通常是UIPrintPageRenderer子类对象. 当UIKit请求打印时,绘制用于打印的内容. 它还可以在页眉页脚中绘制内容. 自定义page renderer必须重写至少一个draw方法, 如果它正在绘制部分内容(不包括页眉页脚), 则它必须计算并返回打印作业的页数. (注意你可以使用UIPrintPageRender类型对象连接一系列打印模式)

  6. (可选)如果你使用page renderer, 那么用UIPrintPageRender的具体子类来创建一个或多个对象, 然后通过addPrintFormatter:startingAtPageAtIndex:方法来给页面设置具体的page render对象. 你可以将一组page renderer对象赋值给属性printFormatters.

  7. 如果当前的设备时iPad, 可以通过presentFromBarButtonItem:animated:completionHandler:presentFromRect:inView:animated:completionHandler:来展示打印界面, 如果是iPhone或者iPod, 那么可以通过presentAnimated:completionHandler:方法. 另外, 可以通过printInteractionControllerParentViewController:delegate方法来将打印界面嵌入到现在的界面中. 如果你的APP使用了一个活动表(iOS 6及以后版本), 你可以添加一个打印活动项.

到这, 步骤的不同取决于你是否打印静态内容, 是否使用print formatter或者page renderer.

将打印机中准备打印的内容打印出来


iOS打印系统接受某些对象并直接打印他们的内容, 该过程中系统会尽量减少APP的参与. 这些对象是NSData, NSURL, UIImage, 和ALAsset类的实例, 它们必须包含或引用图片数据或PDF数据. 图像数据可以涉及所有这些对象类型, PDF数据有NSURL对象引用或有NSData对象包含. 这里有些对这些对象额外的要求:

  • 图像必须以图像I/O框架支持的格式。有关这些格式的列表,请参阅UIImage类API中的支持图像格式。
  • NSURL对象必须使用file:, assets-library:scheme, 或者任何可以返回具有注册协议(registered protocol)的NSData的内容(例如,QuickLook的x-apple-ql-id:)
  • 将打印机就绪对象赋值给类UIPrintInteractionController单例的printingItemprintingItems属性。您可以将一个打印机就绪对象赋值给printingItem和将打印机准备对象数组赋值给printingItems属性。

注意:通过提供打印机就绪对象, 会将打印内容的布局交由打印系统控制, 所以设置诸如打印方向等是没有效果的. 如果你的APP想控制布局, 你必须自己做绘图.
另外, 如果APP对其打印内容使用printingItems保存, 则无法在printer-options视图中指定页面范围, 即使有多个页面, 并且showsPageRange属性值为YES.

在将对象分配给这些属性之前,应该使用UIPrintInteractionController的类方法之一来验证对象。例如,如果具有图像的UTI,并且希望验证图像是否已准备好打印机,则可以首先使用printableUTIs类方法测试它,该方法返回对打印系统有效的一组UTI:

if ([[UIPrintInteractionController printableUTIs] containsObject:mysteryImageUTI])
    printInteractionController.printingItem = mysteryImage;

类似地, 在设置printingItemsprintingItem属性前, 你可以使用UIPrintInteractionControllercanPrintURL:canPrintData:类方法来验证NSURLNSData对象是否可以直接被打印, 尤其是在打印PDF时.

代码6-1展示在打印包含PDF数据的NSData对象之前, 验证是否有效的方法. 并且在打印选项界面显示提供让用户控制page-range的选项.
代码6-1 打印PDF文档同时提供page-range选项的能力

- (IBAction)printContent:(id)sender {
    UIPrintInteractionController *pic = [UIPrintInteractionController sharedPrintController];
    if  (pic && [UIPrintInteractionController canPrintData: self.myPDFData] ) {
        pic.delegate = self;
 
        UIPrintInfo *printInfo = [UIPrintInfo printInfo];
        printInfo.outputType = UIPrintInfoOutputGeneral;
        printInfo.jobName = [self.path lastPathComponent];
        printInfo.duplex = UIPrintInfoDuplexLongEdge;
        pic.printInfo = printInfo;
        pic.showsPageRange = YES;
        pic.printingItem = self.myPDFData;
 
        void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) =
           ^(UIPrintInteractionController *pic, BOOL completed, NSError *error) {
             self.content = nil;
             if (!completed && error)
                  NSLog(@"FAILED! due to error in domain %@ with error code %u",
                  error.domain, error.code);
        };
        if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [pic presentFromBarButtonItem:self.printButton animated:YES
            completionHandler:completionHandler];
        } else {
        [pic presentAnimated:YES completionHandler:completionHandler];
    }
}

处理多个打印对象步骤和上面一样, 唯一区别是设置printingItems属性,而不是printingItem:

pic.printingItems = [NSArray arrayWithObjects:imageViewOne.image, imageViewTwo.image, imageViewThree.image, nil];

使用Print Formatters和Page Renders


Print formatter和page renderer是用来在多页中布局打印内容的对象. 通过starting page, 内容区域, 内容它们可以设置开始页的内容和技术最后一页的内容. 也能够设置也中打印区域的margin. 两者间的区别有:

  • Print formatter只能布局单页内容(一段文本或HTML, 一个view等等)
  • Page renderer可以让你添加页眉页脚, 进行自定义绘制等操作, 如果需要,Page renderer可以使用 print formatters对象来完成他们的大部分工作

如果您创建了UIPrintPageRenderer的自定义子类,您可以部分或全部绘制每个页面的可打印内容。Page renderer可以具有一个或多个与可打印内容的特定页面或页面范围相关联的print formatters

为打印作业设置布局属性

为了确定打印内容在页面上的区域, UIPrintFormatter类为实体子类定义了4个关键的属性. 这些属性和类UIPrintPageRenderderfooterHeightheaderHeight, 类UIPrintPaperpaperSizeprintableRect属性, 共同决定了多页打印任务的布局. 图6-8描述了该布局.

属性 描述
contentInsets 可打印矩形区域内的点到顶部,左侧,右侧三边的距离, 这些值设置打印内容的内边距(margin), 虽然可以被maximumContentHeight和maximumContentWidth值所覆盖. 顶部边距只对第一页有效.
maximumContentHeight 设置内容区域的最大高度, 包括了页眉页脚的高度. UIKit会比较该属性和页面高度-顶部边距(topInset)的值, 然后取较小的值.
startPage 用于formatter开始绘制的内容页. 此值是从0开始的, 比如page renderer告诉formatter开始打印第三页, 那么page renderer会将startPage设置为2
maximumContentWidth 设置内容区域的最大宽度. UIKit会比较该属性和页面高度-左边距(leftInset)的值-右边距(rightInset), 然后取较小的值.

注意:如果你使用UIPrintInteractionController的属性printFormatter, 那么UIPrintPageRenderer对象将无效了, 也即是说, 你无法设置页眉页脚了.

图6-8 多页打印的布局

UIPrintFormatter会使用上表的四个属性,来计算打印内容的页数, 然后将页数保存在只读属性pageCount中.

使用Print Formatter

UIKit可以让你设置打印job的单个print formatter. 如果你有纯文本或HTML文档, 这可能是一个有用的功能, 因为UIKit为这些类型的文本内容定义了print formatter实体子类. 这些子类还可以让你打印UIKit视图中的内容.
UIPrintFormatter类是系统为打印格式化定义的虚基类. 目前, iOS提供了以下实体的UIPrintFormatter子类:

  • UIViewPrintFormatter-自动在多个页面上显示视图的内容. 若要获取视图的print formatter对象, 请使用view的viewPrintFormatter方法.
    • 并非所有的内置UIKit视图都支持打印. 目前只有UIWebView, UITextView,MKMapView这几个视图知道如何绘制打印的内容.
    • 视图的print formatter不应该用来打印自定义视图. 如要打印自定义视图的内容, 请使用UIPrintPageRenderer替代.
  • UISimpleTextPrintFormatter-自动绘制和布局纯文本文档. 该formatter允许你设置text的全局性属性, 如font, color, alignment, 已经换行模式.
  • UIMarkupTextPrintFormatter-自动绘制和布局HTML文档.

注意:UIPrintFormatter类并不让自定义继承, 如果需要自定义布局, 你可以使用page renderer

尽管下面的内容还是使用单个print formatter(并且没有page renderer), 但是在关于打印 formatter的许多信息都适用于和page renderer结合使用.

打印纯文本或HTML

虚度APP包含用户想打印的文本内容. 如果内容为纯文本或HTML文本, 那么你可以访问屏幕显示内容后面的字符串, 然后使用UISimpleTextPrintFormatterUIMarkupTextPrintFormatter来布局和绘制这些内容. 你只需要使用这些内容的后面的字符串来创建print formatter实例, 然后设置layout属性. 再将该print formatter赋值到UIPrintInteractionController的实例.

代码6-2告诉你使用一个UIMarkupTextPrintFormatter对象来打印HTML文档. 它会在打印区域内加上内边距. 你可以通过属性printPaper来确定打印区域. 想看更详细的代码参考苹果官方demoUIKit Printing with UIPrintInteractionController and UIViewPrintFormatter
代码清单6-2 打印HTML文档

- (IBAction)printContent:(id)sender {
    UIPrintInteractionController *pic = [UIPrintInteractionController sharedPrintController];
    pic.delegate = self;
 
    UIPrintInfo *printInfo = [UIPrintInfo printInfo];
    printInfo.outputType = UIPrintInfoOutputGeneral;
    printInfo.jobName = self.documentName;
    pic.printInfo = printInfo;
 
    UIMarkupTextPrintFormatter *htmlFormatter = [[UIMarkupTextPrintFormatter alloc]
        initWithMarkupText:self.htmlString];
    htmlFormatter.startPage = 0;
    htmlFormatter.contentInsets = UIEdgeInsetsMake(72.0, 72.0, 72.0, 72.0); // 1 inch margins
    pic.printFormatter = htmlFormatter;
    pic.showsPageRange = YES;
 
    void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) =
         ^(UIPrintInteractionController *printController, BOOL completed, NSError *error) {
             if (!completed && error) {
                 NSLog(@"Printing could not complete because of error: %@", error);
             }
         };
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [pic presentFromBarButtonItem:sender animated:YES completionHandler:completionHandler];
    } else {
        [pic presentAnimated:YES completionHandler:completionHandler];
    }
}

使用UISimpleTextPrintFormatter对象布局和打印纯文本文档的过程几乎是相同的。但是,这个对象的类包括允许您设置打印文本的字体、颜色和对齐方式的属性。

打印view的中内容

你可以使用UIViewPrintFormatter实例来布局和打印系统view的内容. UIKit会自动为创建该view的print formatter对象. 通常视图绘制的代码和视图print formatter中的绘制打印内容代码一样. 目前iOS支持视图打印的view有UIWebView, UITextView, MKMapView. 为了获取视图的print formatter对象你需要调用view的viewPrintFormatter方法, 然后设置formatter的layout和starting page等属性并将formatter设置为打印交互控制器的printFormatter属性. 另外, 如果你想自己绘制部分的打印内容, 那么你可以将该view的print formatter赋值给UIPrintPageRenderer对象. 代码6-3展示了使用view的print formatter来打印UIWebView中的内容.
代码清单6-3 打印webView中的内容

- (void)printWebPage:(id)sender {
    UIPrintInteractionController *controller = [UIPrintInteractionController sharedPrintController];
    void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) =
        ^(UIPrintInteractionController *printController, BOOL completed, NSError *error) {
        if(!completed && error){
            NSLog(@"FAILED! due to error in domain %@ with error code %u",
            error.domain, error.code);
        }
    };
    UIPrintInfo *printInfo = [UIPrintInfo printInfo];
    printInfo.outputType = UIPrintInfoOutputGeneral;
    printInfo.jobName = [urlField text];
    printInfo.duplex = UIPrintInfoDuplexLongEdge;
    controller.printInfo = printInfo;
    controller.showsPageRange = YES;
 
    UIViewPrintFormatter *viewFormatter = [self.myWebView viewPrintFormatter];
    viewFormatter.startPage = 0;
    controller.printFormatter = viewFormatter;
 
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [controller presentFromBarButtonItem:printButton animated:YES completionHandler:completionHandler];
    }else
        [controller presentAnimated:YES completionHandler:completionHandler];
}

使用Page Renderer

一个页面渲染器是UIPrintPageRenderer的子类的实例, 它绘制打印作业的全部或部分内容. 若要使用需要子类化它, 并将添加子类到项目中, 然后将该子类的实例赋值到打印交互控制器的UIPrintPageRenderer属性. 一个page renderer可以和一个或多个print formatter相关连, 这样可以将内容绘制和print formatter的绘制结合使用.

Page renderer可绘制和布局打印的内容, 也可使用print formatter来处理特定页面上的内容. 因此, 为了获取多个formatter, 你可以使用UIPrintPageRenderer类自身来和多个formatter相关联. 但大多数情况下是使用UIPrintPageRenderer的子类.

注意:如果要打印页眉页脚信息, 比如, 重复文档标题和递增显示页数. 则必须使用UIPrintPageRenderer的自定义子类

UIPrintPageRenderer基类包含页面计数和页眉页脚高度这些属性. 它还声明了几个方法, 重写这些方法可以绘制特定部分的:页眉, 页脚, 内容本身, 或者结合page render和formatter来绘制内容.

设置Page renderer的Attributes

如果使用page renderer绘制每页的页眉页脚时, 你需要设置页眉页脚的高度. 你可以通过属性headerHeightfooterHeight来设置. 如果这些高度属性的值为0(默认), 那么drawHeaderForPageAtIndex:InRect:drawFooterForPageAtIndex:inRect:方法不会被调用.

如果你的page renderer打算去绘制页面中的内容, 也就是说, 在页眉和页脚之间的那部分区域. 那么你应该在子类中重写numberOfPages方法, 在方法中计算和返回需要绘制的页数. 如果你的page renderer关联了一个print formatter, 那么print formatter会计算页面的数量, 并绘制除了也没和页脚以外的所有内容.

如果单独使用page renderer时, 每页的布局都需要你自己考虑. 在计算layout时, 你可以考虑headerHeight, footerHeight, paperRectprintableRect这四个UIPrintPageRenderer的属性(后面两个只读)值. 如果你结合print formatter使用时, 你需要同时考虑的布局因素还包括contentInset, maximumContentHeightmaximumContentWidth这几个UIPrintFormatter的属性值.

实现Page renderer中的绘图方法

当APP使用page renderer去绘制可打印的内容时, UIKit会为请求的内容的每一页调用以下方法. 注意, UIKit不会保证按页顺序去调用这些方法. 另外如果用户请求打印部分页面(即, 他们制定了页面范围), UIKit只会为需要打印的页面调用这些方法.

注意: 如果你绘制image或者PDF这些可打印内容时, 使用的是非UIKit API(比如, Quartz API), 那么你需要翻转UIKit的坐标系, 将坐标系改为LLO型的.

drawPageAtIndex:inRect:方法中会按照下表的列表方法的顺序来调用其他方法. 如果你想全面控制打印内容的绘制可以将下面这些方法重写.

重写... 可以...
drawHeaderForPageAtIndex:inRect: 绘制页眉的内容. 当headerHeight为0时, 该方法不会调用.
drawContentForPageAtIndex:inRect: 绘制打印作用的内容(即页眉页脚间的区域)
drawPrintFormatter:forPageAtIndex: 将自定义绘图和有print formatter执行的绘图混合. 当给每一页关联一个print formatter时, 该方法会被调用.
drawFooterForPageAtIndex:inRect: 绘制页脚内容. 当footerHeight为0时, 该方法不会调用.

所有的这些绘图方法都是在当前绘图上下文(由UIGraphicsGetCurrentContext函数返回)中绘制. 传入这些绘图方法的rect参数定义的区域包含页面页脚, 内容区域, 整个页面区域, 以及页面的origin, 基于ULO坐标系.

代码6-4举例说明drawHeaderForPageAtIndex:inRect:drawFooterForPageAtIndex:inRect:方法的实现. 它们使用CGRectGetMaxXCGRectGetMaxY来计算在打印区域坐标系中的footerRectheaderRect中的文本位置.
代码清单6-4 绘制页眉页脚

- (void)drawHeaderForPageAtIndex:(NSInteger)pageIndex  inRect:(CGRect)headerRect {
    UIFont *font = [UIFont fontWithName:@"Helvetica" size:12.0];
    CGSize titleSize = [self.jobTitle sizeWithFont:font];
    //center title in header
    CGFloat drawX = CGRectGetMaxX(headerRect)/2 - titleSize.width/2;
    CGFloat drawY = CGRectGetMaxY(headerRect) - titleSize.height;
    CGPoint drawPoint = CGPointMake(drawX, drawY);
    [self.jobTitle drawAtPoint:drawPoint withFont: font];
}
 
- (void)drawFooterForPageAtIndex:(NSInteger)pageIndex  inRect:(CGRect)footerRect {
    UIFont *font = [UIFont fontWithName:@"Helvetica" size:12.0];
    NSString *pageNumber = [NSString stringWithFormat:@"%d.", pageIndex+1];
    // page number at right edge of footer rect
    CGSize pageNumSize = [pageNumber sizeWithFont:font];
    CGFloat drawX = CGRectGetMaxX(footerRect) - pageNumSize.width - 1.0;
    CGFloat drawY = CGRectGetMaxY(footerRect) - pageNumSize.height;
    CGPoint drawPoint = CGPointMake(drawX, drawY);
    [pageNumber drawAtPoint:drawPoint withFont: font];
}
将一个或多个Print formatter和一个Page renderer配合使用

一个page renderer可以和一个或多个print formatter结合使用来绘制打印内容. 比如, APP可能会使用UISimpleTextPrintFormatter对象来绘制打印页中的文本内容, 但是使用一个page renderer来在每页的页眉中绘制文档标题. 或者, APP使用两个print formatter, 一个用来绘制第一页header信息和开始部分内容, 另一个用来绘制页面中的其他内容, 然后才会使用page renderer来绘制两部分的分割线.

正如前面所讲, 可以将单个print formatter赋值给UIPrintInteractionController的属性printFormatter. 但是如果你使用一个page renderer和print formatter的话, 你需要做如下两件事来将print formatter和page renderer关联起来:

  • 用一个数组将print formatter装起来, 然后将数组赋值给printFormatters
  • 使用addPrintFormatter:startingAtPageAtIndex:方法来添加每个print formatter

在将print formatter设置到page renderer之前, 要将print formatter的layout属性设置好, 包括打印作业的起始页(startPage). 当你设置这些属性后, UIPrintFormatter会计算打印任务的页数. 注意如果你通过方法addPrintFormatter:startingAtPageAtIndex:方法来添加起始页, 那么属性startPage的值会被覆盖.

page renderer可以将自己的绘制操作和print formatter绘制操作的结合起来对某个页进行绘制. page renderer绘制print formatter无法绘制的内容, 然后调用drawInRect:forPageAtIndex:方法来绘制余下的部分内容. page render也可以绘制一种"遮盖"效果, 方法时先让print formatter绘制内容, 然后在内容上面绘制一层内容.

测试APP的内容打印


苹果官方说iOS4.2之后提供了一个Printer Simulator供开发者测试, 集成在Xcode中, 但我没找到!!! 网上应该可以下载.

常见的打印任务


打印的大部分任务基本交由系统完成, APP需要做的是, 确认设备是否具有打印功能, 总结打印选项界面. 然后提供一个打印按钮供用户进入打印界面, 之后为该按钮绑定一个打印事件. 所以该按钮的响应方法是关键, 也是我们APP需要自己处理的关键部分, 下面内容将如何实现打印按钮的事件方法.

测试打印能力

有些iOS设备是不支持打印的. 所以在要写打印代码之前你需要判断是否支持打印. 如果该iOS设备不支持打印, 你不能显示任何有关打印的界面. UIPrintInteractionController的类方法isPrintingAvailable可以判断当前设备是否支持打印. 代码6-5展示了打印能力的判断代码示例.
代码清单6-5 更加打印能力判断是否显示打印按钮

- (void)viewDidLoad {
    if (![UIPrintInteractionController isPrintingAvailable])
        [myPrintButton removeFromSuperView];
    // other tasks...
}

注意:上面直接移除按钮, 而不是影藏或者disable等操作, 因为如果设备不支持打印, 那么该设备就不会有可能支持的可能了, 所以直接移除是推荐的

设置打印作业信息

一个UIPrintInfo对象包含了很多有关打印作业的信息, 尤其是:

  • output type(输出类型, 指明了内容的类型)
  • print-job name(名字)
  • printing orientation(朝向)
  • duplex mode(单双页打印)
  • identifier of the selected(选择的打印记得标识符)

你不必对UIPrintInfo的所有属性赋值, 你只需要选择几个需要的关键属性赋值, 其他属性UIKit会帮你设置为默认值.(如果可以, 你都不必创建UIPrintInfo对象, 但不建议这样做, why? 前面讲过)

在大多数情况下, 你会对打印作业设置一些信息, 比如output type. 你可以通过类方法printInfo来获取UIPrintInfo对象, 然后对该对象设置一些属性. 将设置好的对象复制给UIPrintInteractionControllerprintInfo属性. 代码6-6, 展示了这一过程.
代码清单6-6 设置printInfo属性

UIPrintInteractionController *controller = [UIPrintInteractionController sharedPrintController];
UIPrintInfo *printInfo = [UIPrintInfo printInfo];
printInfo.outputType = UIPrintInfoOutputGeneral;
printInfo.jobName = [self.path lastPathComponent];
printInfo.duplex = UIPrintInfoDuplexLongEdge;
controller.printInfo = printInfo;

UIPrintInfo的一个属性是用来控制打印方向的:portrait(竖立)和landscape(横向). 你可以选择合适的方向来打印你的内容, 另外, 当打印的内容的宽度大于高度的话, 推荐使用横向(landscape)打印方向. 代码6-7,展示了设置打印方向
代码6-7 设置打印方向

UIPrintInteractionController *controller = [UIPrintInteractionController sharedPrintController];
// other code here...
UIPrintInfo *printInfo = [UIPrintInfo printInfo];
UIImage *image = ((UIImageView *)self.view).image;
printInfo.outputType = UIPrintInfoOutputPhoto;
printInfo.jobName = @"Image from PrintPhoto";
printInfo.duplex = UIPrintInfoDuplexNone;
// only if drawing...
if (!controller.printingItem && image.size.width > image.size.height)
    printInfo.orientation = UIPrintInfoOrientationLandscape;

设置纸张的大小, 方向, 单/双页打印

默认情况下, UIKit根据目标打印机和打印作业的输出类型设置一组默认的纸张大小. 如, 根据UIPrintInfo中的output type来指定. 例如, 如果输出是类型UIPrintInfoOutputPhoto, 默认纸张大小是4x6英寸, A6或其他一些标准, 这和当地的一些标准有关. 如果输出类型是UIPrintInfoOutputGeneralUIPrintInfoOutputGray, 则默认大小是US 字母(8 1/2x11英寸), A4或一些其他标准的大小设置.

对于大多数APP来说, 这些默认纸张是可以接受的. 然而有些APP可能需要特殊的纸张尺寸. 比如生成宣传小册子或者贺卡等等, 那么就需要定制自己的纸张尺寸. 在这种情况下, 打印交互控制器的委托可以实现UIPrintInteractionControllerDelegateprintInteractionController:choosePaper:方法来返回一个UIPrintPaper对象, 该对象表示可用纸张和可打印矩形区域的最佳组合. delegate有两个方式来找到合适的UIPrintPaper对象, 它可以检查传入的UIPrintPaper数组来找到一个最合适的对象. 或者delegate可以通过UIPrintPaper的类方法bestPaperForPageSize:withPapersFromArray:来找到最佳对象. 代码6-8展示了delegate方法的实现
代码清单6-8 方法bestPaperForPageSize:withPapersFromArray:的实现

- (UIPrintPaper *)printInteractionController:(UIPrintInteractionController *)pic
    choosePaper:(NSArray *)paperList {
    // custom method & properties...
    CGSize pageSize = [self pageSizeForDocumentType:self.document.type];
    return [UIPrintPaper bestPaperForPageSize:pageSize
        withPapersFromArray:paperList];
}

通常, 使用自定义 page renderer在会计算页面数量时,计算纸张的大小. 如果APP需要向用户展示纸张大小的选项, APP则必须自己实现该UI界面, 然后在打印交互控制器中使用用户选择的纸张. 例如:

// Create a custom CGSize for 8.5" x 11" paper.
CGSize custompapersize = CGSizeMake(8.5 * 72.0, 11.0 * 72.0);

UIPrintInfo类还允许您提供其他设置,例如打印方向、选择的打印机和单双页模式(如果打印机支持双页打印).

将打印集成到用户界面中

这里有两个方式将打印集成到用户界面中:

  • 使用打印交互控制器UIPrintInteractionController
  • 从activity sheet中展示(iOS6.0及以后)
使用打印交互控制器来呈现打印选项界面

UIPrintInteractionController声明了三种展示打印选项界面的方法, 没种都有自己的动画:

  • presentFromBarButtonItem:animated:completionHandler:从导航栏或toolbar上的按钮上popover一个view出来(通常是从打印按钮的位置出来)
  • presentFromRect:inView:animated:completionHandler:从一个view中的指定rect区域中popover
  • presentAnimated:completionHandler:从屏幕底部向上滑出一个sheet页面

前两个方法针对的是iPad设备, 第三个方法时针对iPod和iPhone设备. 你在代码中可以通过判断UI_USER_INTERFACE_IDIOM常量值是否为UIUserInterfaceIdiomPadUIUserInterfaceIdiomPhone来判断使用哪一个方法. 代码6-9展示该判断
代码清单6-9 根据设备类型来使用presenting方法

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
    [controller presentFromBarButtonItem:self.printButton animated:YES
        completionHandler:completionHandler];
} else {
    [controller presentAnimated:YES completionHandler:completionHandler];
}

如果你在iPad中调用了iPhone类的presenting方法, 那么会从当前window的frame中popover一个view, 如果你在iPhone设备中调用了iPad的presenting方法, 那么会从底部slide up一个sheet页面.

如果你调用presenting方法来显示一个打印选项界面, 但打印选项界面已经显示了, 那么UIPrintInteractionController会隐藏打印选项界面, 你必须重新调用presenting方法来显示一个打印选项界面.

如果你设置了打印机的ID,或单双页设置,那么在打印选项界面, 默认选择你设置的值. 如果你让用户选择打印页面范围, 那么你必须对打印交互控制器的showsPageRange属性设置为YES(默认是NO). 但是如果你通过属性printingItems来进行打印, 或者打印的页数为1, 那么即使你将showsPageRange设置为YES.

如果你想打印界面显示在一个特殊的view中, 那么你可以实现UIPrintInteractionControllerDelegate方法printInteractionControllerParentViewController:, 该方法需要你提供打印交互控制器的父控制器.

从activity sheet中显示打印

要使用activity sheet显示打印界面也简单, 但注意这里有两个警告:

  • APP无法控制用户是否可以选择页面范围
  • APP不能使用delegate方法来重写打印行为, 列如手动覆盖纸张大小选择的行为.

使用此技术, APP必须创建包含以下内容的activity items的数组:

  • 一个UIPrintInfo对象
  • 一个page renderer对象, print formatter对象, printable item
  • 任何其他activity item, 只要你觉得可以

然后你调用initWithActivityItems:applicationActivities:方法, 第一次参数传activity items数组, 第二参数为nil. 最后, 你使用标准的控制器present*方法, 将activity sheet present出来. 具体细节可看UIActivityViewController Class ReferenceUIActivity Class Reference

响应打印作业的完成和错误

前面已经讲过如何present打印交互控制器, 其中有个参数我们没讲到就是completionHandler. 这个参数的类型是UIPrintInteractionCompletionHandler,一个block. 用来结果回调处理的. 通常可以在这个block中处理打印成功/失败的信息. 比如打印失败可以,将原因分析出来, 然后显示给用户查看. 代码6-10, 展示了completionHandler的实现

代码清单6-10 实现completionHandler block

void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) =
        ^(UIPrintInteractionController *pic, BOOL completed, NSError *error) {
    self.content = nil;
    if (!completed && error)
        NSLog(@"FAILED! due to error in domain %@ with error code %u",
            error.domain, error.code);
  };

当打印结束后, UIKit会将UIPrintInteractionController拥有的实例释放掉, 所以不需要你在completionHandler中做这些工作. 如果打印失败了, 在completionHandler中会传入NSError, 有个domain为UIPrintErrorDomain和错误码, 这些都会定义在UIPrintError.h中. 在几乎所有的情况下,这些错误码指示的是编程错误,因此通常不需要通知用户。但也会由于打印文件(由文件方案NSURL对象所定位)导致错误, 这可能需要提示用户.

推荐阅读更多精彩内容