资讯专栏INFORMATION COLUMN

如何使用HTML模版和iOS中的UIPrintPageRenderer来生成PDF文档

dreamtecher / 2740人阅读

摘要:如何使用模版和中的来生成文档作者,时间翻译,如有错误欢迎指出。其中一些可以手动设置,一些通过计算得到,还有一些通过代码进行硬编码。如前所诉,在中并不能设置所有的发票信息都会采用硬编码的方式。

如何使用HTML模版和iOS中的UIPrintPageRenderer来生成PDF文档

作者:GABRIEL THEODOROPOULOS,时间:2016/7/10
翻译:BigNerdCoding, 如有错误欢迎指出。原文链接

你是否曾经被要求过在app中直接将内容生成为PDF文档?如果没有的话,你是否思考过这个需求该如何实现呢?

虽然使用提问的方式作为文章开头有点不按套路出牌,但是这些问题就是本文要讨论的重点。在app中创建PDF文档,看起来就是一条布满坑的路,但是事实上可能并没有那么恐怖。作为开发者,在面对困难的时候我们总是需要一些替换方案,避免一条道走到黑。手动生成PDF页面确实是一个非常痛苦的过程(取决于文档的内容)并且最终可能会是事倍功半的结果。计算位置、添加线、配色、插入、偏移等等,可能有趣(也可能没有)。但是如果文档内容复杂的话,那么肯定是一件坑爹的事。不太可能有人喜欢干这样的事。

在本文中我会给你介绍一种新思路来创建PDF文档,并且比手动绘制要简单不少。处理方法是基于使用HTML templates,并且可以概括为以下几步:

为那些需要打印为PDF的表单或者内容创建HTML templates

使用上面的HTML templates来生成真实的内容(可以在web view中进行预览)

将HTML内容打印为PDF文档

最后一步由iOS系统来完成。

我想你也一定会赞同处理HTML比直接绘制PDF文档更容易一些。在这种情况下,你只需要将你的文档处理成一个HTML页面就行了,当然对重复内容手动创建HTML也很低效。例如,如果我们的app要将学生信息打印或者导出为PDF文档。因为每个学生的信息格式是一样的,为每一个学生创建多带带的HTML页面显然并不可取。理想的做法是创建一个HTML页面作为模版,然后使用“占位符”来表示那些需要打印的信息。然后在你的app里面,我们再使用真实信息来替换掉占位符,而且这种处理可以重复进行。

当你将那些真实信息表示为HTML代码后,你可以做任何HTML支持的功能。这意味着你可以在一个WebView中展示内容,将其保存为外部文件,分享内容,当然还有将其打印为PDF文档。

所以,文章接下来的内容是什么呢?

本文最终目标是让你知道如何将内容生成为一个PDF文档。但是首先我们需要将HTML模版中的“占位符”替换为真实信息。文中的演示应用功能就是打印发票,这与现实中PDF文档打印需求相符。当然一些默认的功能已经给出了,我们不需要从头开始构建整个应用,毕竟那并不是文章的目的。在起始工程中已经有了HTML模版,后面会对模版中的内容做介绍,这样你就能知道那些“占位符”所代表的真实含义并对模版整体有清晰的认识。不管怎样,我们都要一步步来实现最终的目标:生成HTML并将其打印为PDF文档。除此之外,我还会给你展示如何在最终的PDF文档中添加页眉、页脚。

是不是想想都激动?好戏开场了!

起始工程

接下来,我们会快速的浏览这个发票打印工具的Demo。在开始之前,你需要先去下载工程代码文件并打开工程。

你会发现该工程中的很多功能已经实现了。运行程序,首先看到的就是用来展示新建发票的视图控制器InvoiceListViewController。在该视图控制器中你可以通过右上角的+按键来创建新的发票。点击该视图中的任一发票就会跳转到预览视图。在预览视图中我们需要实现PDF文档的预览和打印功能。当然,预览视图里面的功能还等着我们去完成,这也是文章的重点。最后,在展示视图中我们可以通过左划来实现对发票的删除操作,具体看下面演示截图:

如上所说,点击新建按键后Demo会跳转到CreatorViewController视图中完成新增发票的功能。界面如下:

在生成订单之前,我们需要填写很多信息。其中一些可以手动设置,一些通过计算得到,还有一些通过代码进行硬编码。其中需要手动添加的信息有:

recipient info是发票收件人的地址,对应上图中的灰色区域。

invoice items对应一个发票中具体项目,主要由服务提供商和服务费组成。为了程序的简洁性,这里并没有设置增值税。使用屏幕下方的+按键实现添加(更多内容等会再说)。

程序计算得到的信息:

发票单号(导航栏上的标题)

总共的发票金额(左下角)

需要硬编码的部分:

寄件人信息

发票到期日(这里默认设置为空,你也可以自己定制)

付款方式

发票的Logo

针对invoice items我们可以在AddItemViewController视图中进行数据录入。录入的数据包括服务描述和价格,维护好数据后可以点击保存回到前一个视图。

每个新建的发票子项的信息都被存放在一个字典的结构中,并被追加到数组中。该数组也是CreatorViewController视图中tableview的datasource。当一个发票保存后,所有的子项和计算得到的信息都会被保存到字典中并返回到InvoiceListViewController中,返回的信息包括:

发票编号

收件人信息

总金额

发票中包含的具体子项

保存完该发票后我们会计算一个新的编号并设置到NSUserDefaults中,以便后面的继续使用。每一次用户创建新发票后,返回的信息以dictionary类型追加到InvoiceListViewController里的数组中并且该数组也会被保存到NSUserDefaults中。在该视图的viewWillAppear中我们会将信息重新加载出来。请注意:这里之所以将信息保存到 NSUserDefaults 中,主要是因为对于演示app来说这个方案简单。但是在真实的app开发时不建议这样做,毕竟存在很多更好的方案。

对于现有的代码我并没有做什么分析,你可以自己去每个视图中跟着流程去查看具体的细节。唯一我希望大家注意的是AppDelegate.swift。里面有获取application delegate、文档目录、获取金额对应货币字符串表示的三个convenient方法,在后面的代码中还会使用到它们。还有我们通过currencyCode将默认货币单位设置为乐"eur",你可以自行修改。

最后,我来说下起始工程中需要我们在后面继续完成的功能。当我们点击InvoiceListViewController中tableview的某一行发票的时候,PreviewViewController会收到包含发票信息的dictionary类型数据。在这个视图控制器里面我们会使用webview来展示HTML格式的发票内容,并且点击导出按键生成对应的PDF文档。这些功能需要我们来实现,不过我们需要确保PreviewViewController已经有可以直接使用的发票数据。

HTML模版文件

正如在前面介绍的那样,我们会先用HTML模版对发票数据做初步处理,然后将生成的真实HTML内容打印为PDF文件。这里的主要操作方法是:先在HTML模版文件中设置一些“占位符”,然后将需要展示的信息替换这些“占位符”。为了实现这一目的首先就是要创建符合展示效果的自定义模版。但是本文的关注点并不是这个,所以我们会使用一个已有的模版[地址]3。本文已经对模版做了一些修改,去除了边界和阴影并给logo添加了灰色背景。

在你下载的起始工程里面,你可以看见下面三个HTML模版文件:

invoice.html

last_item.html

single_item.html

每个模版文件中的“占位符”都会用#符号进行标记。例如,下面的内容就展示了发票编号、签发日期和失效日期的“占位符”:

> Invoice #: #INVOICE_NUMBER
#INVOICE_DATE#
#DUE_DATE#

注意:虽然在模版中有失效日期的“占位符”,但在文中我们并不会真的用到。我们会使用一个空字符串来替换这个“占位符”,当然如果你想使用也没有任何问题。

你可以在三个模版文件中找到所有的“占位符”以及它们的位置。下面列出全部的“占位符”:

LOGO_IMAGE

INVOICE_NUMBER

INVOICE_DATE

DUE_DATE

SENDER_INFO

RECIPIENT_INFO

PAYMENT_METHOD

ITEMS

TOTAL_AMOUNT

ITEM_DESC

PRICE

最后两个“占位符”只在single_item.html和last_item.html模版文件中。当然,invoice.html模版中的#ITEMS#占位符会被其他两个模本文件创建的子项的代码替换掉。

如你所见,为输出的内容创建一个或者多个HTML模版并不是件困难的事情。并且当我们完成这部分工作之后,剩下的基于模版生成真实信息并将其导出为PDF文件将会变的很轻松。

给内容排版

一系列准备工作完成后,接下来就是动手完成缺失的关键功能了。第一步,我们需要使用模版将InvoiceListViewController中的选中行的发票信息生成为HTML文件。完成这步后,接下来会在PreviewViewController中使用webview将内容展示出来,以验证功能是否实现了。

这里最主要也是最重要的任务就是:必须将模版中的"占位符"正确的替换为发票中的真实信息。在后面你会发现这一步的处理是非常直接和简单的。但是在此之前,我们先新建一个类用于生成真实的HTML文件和后面的PDF打印操作。所以我们创建一个继承自NSObject的类:InvoiceComposer

打开新建的类文件并声明一些常量和变量属性:

class InvoiceComposer: NSObject {

    let pathToInvoiceHTMLTemplate = NSBundle.mainBundle().pathForResource("invoice", ofType: "html")

    let pathToSingleItemHTMLTemplate = NSBundle.mainBundle().pathForResource("single_item", ofType: "html")

    let pathToLastItemHTMLTemplate = NSBundle.mainBundle().pathForResource("last_item", ofType: "html")

    let senderInfo = "Gabriel Theodoropoulos
123 Somewhere Str.
10000 - MyCity
MyCountry" let dueDate = "" let paymentMethod = "Wire Transfer" let logoImageURL = "http://www.appcoda.com/wp-content/uploads/2015/12/blog-logo-dark-400.png" var invoiceNumber: String! var pdfFilename: String! }

前三个属性对应三个HTML模版的文件路径。这些文件路径信息能方便后面的文档信息的读写操作。

如前所诉,在Demo中并不能设置所有的发票信息(senderInfo, dueDate, paymentMethod, logoImageURL都会采用硬编码的方式)。当然在真实的应用中这些信息应该是可以被用户设置和修改的。紧接着的属性是为发票选定的logo的链接,你也可以对这些的信息进行修改。

最后,invoiceNumber属性对应在当前预览的发票编号,而pdfFilename对应PDF文件的全路径。还有一些信息我们等到后面要用的时候再来处理。

除了这些属性,还需要添加默认的初始化方法init()

class InvoiceComposer: NSObject {

    ...

    override init() {
        super.init()
    }
}

接下来我们实现处理替换HTML模版“占位符”重任的函数。函数声明如下:

funnc renderInvoice(invoiceNumber: String, invoiceDate: String, recipientInfo: String, items: [[String: String]], totalAmount: String) -> String! {

}

该函数的参数包含了所有使用demo创建出来的发票信息也是程序所需的全部。

现在我们开始动手来完善代码。在下面的代码中有两个重要的步骤,首先我们字符串格式读取了模版文件invoice.html以便后面的修改操作,然后我们替换了除发票子项之外的“占位符”。详见:

func renderInvoice(invoiceNumber: String, invoiceDate: String, recipientInfo: String, items: [[String: String]], totalAmount: String) -> String! {
    // Store the invoice number for future use.
    self.invoiceNumber = invoiceNumber

    do {
        // Load the invoice HTML template code into a String variable.
        var HTMLContent = try String(contentsOfFile: pathToInvoiceHTMLTemplate!)

        // Replace all the placeholders with real values except for the items.
        // The logo image.
        HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#LOGO_IMAGE#", withString: logoImageURL)

        // Invoice number.
        HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#INVOICE_NUMBER#", withString: invoiceNumber)

        // Invoice date.
        HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#INVOICE_DATE#", withString: invoiceDate)

        // Due date (we leave it blank by default).
        HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#DUE_DATE#", withString: dueDate)

        // Sender info.
        HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#SENDER_INFO#", withString: senderInfo)

        // Recipient info.
        HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#RECIPIENT_INFO#", withString: recipientInfo.stringByReplacingOccurrencesOfString("
", withString: "
")) // Payment method. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#PAYMENT_METHOD#", withString: paymentMethod) // Total amount. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#TOTAL_AMOUNT#", withString: totalAmount) } catch { print("Unable to open and use HTML template files.") } return nil }

在代码中,我们通过stringByReplacingOccurrencesOfString(...)函数就轻松的完成了占位符的替换。虽然大量“占位符”的替换操作可能会很烦躁和无聊,但是最起码这个操作并不难。

另外需要注意的是,在使用文件内容初始化一个字符串变量的时候可能会抛出异常,所以上面的操作都是在do-catch结构里完成的。另外,如果出现问题的话我们会返回nil,至于最终需要返回的HTML内容还要下一步处理。

现在将注意力放到发票的子项处理上面。因为子项的数量可能会比较多,我们将采取循环遍历数组来进行处理。最后一项的“占位符”替换会使用last_item.html模版,其他的都将使用single_item.html模版。所有这些子项处理的结果都会被追加到allItems字符串变量中,该变量会被用来替换HTMLContent字符串中的#ITEMS#占位符。最后我们将处理结果返回。

代码如下:

func renderInvoice(invoiceNumber: String, invoiceDate: String, recipientInfo: String, items: [[String: String]], totalAmount: String) -> String! {
    ...

    do {
        ...       

        // The invoice items will be added by using a loop.
        var allItems = ""

        // For all the items except for the last one we"ll use the "single_item.html" template.
        // For the last one we"ll use the "last_item.html" template.
        for i in 0..

注意:getAppDelegategetStringValueFormattedAsCurrency方法的具体实现,我已经在前面提过了。它们都在AppDelegate.swift文件中。

这一步到这里就结束了,我们成功实现了真实发票HTML格式信息的生成。接下来就是对该结果的进一步处理了。

预览处理后的HTML内容

在上一步处理完成后,接下来就需要验证结果是否正确了。因此这一部分内容的目的就是使用PreviewViewController视图中的webview来加载该HTML内容,查看我们前面努力的效果。需要注意的是:在真实的应用中这一步是可选的,我们可以跳过预览直接打印PDF,这里之所以需要预览仅仅是为了Demo的功能完整性而已。

我们在PreviewViewController.swift文件中声明属性:

class PreviewViewController: UIViewController {

    ...

    var invoiceComposer: InvoiceComposer!

    var HTMLContent: String!

}

第一个属性就是新建的类的实例,而HTMLContent属性则是对应最终内容的String类型变量我们会在后面用到它。

接下来我们创建一个函数来实现如下功能:

初始化invoiceComposer对象

调用invoiceComposer对象的renderInvoice(...)函数得到发票的HTML编码内容

在webview中加载该内容

将得到的HTML编码内容赋值给HTMLContent属性

代码如下:

func createInvoiceAsHTML() {
    invoiceComposer = InvoiceComposer()
    if let invoiceHTML = invoiceComposer.renderInvoice(invoiceInfo["invoiceNumber"] as! String,
                                                       invoiceDate: invoiceInfo["invoiceDate"] as! String,
                                                       recipientInfo: invoiceInfo["recipientInfo"] as! String,
                                                       items: invoiceInfo["items"] as! [[String: String]],
                                                       totalAmount: invoiceInfo["totalAmount"] as! String) {

        webPreview.loadHTMLString(invoiceHTML, baseURL: NSURL(string: invoiceComposer.pathToInvoiceHTMLTemplate!)!)
        HTMLContent = invoiceHTML
    }
}

代码很简单,唯一需要注意的是:只有renderInvoice(...)函数返回的内容不是nil的时候才能进行加载、赋值等操作。

下面就是函数调用了:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    createInvoiceAsHTML()
}

如果你想看到显示效果,你可以先去创建一个新发票,然后在列表中点击该发票你就会看见加载后的效果图了。如下:

打印前的准备工作

工作完成了一半接下来该轮到打印部分的处理了,这样才能完成最终导出PDF格式的发票的目标。我们将会使用到UIPrintPageRenderer类。如果你之前没有使用会听说过这个类的话,一句话来说就是:这个类就是用来打印内容的(打印成文件或者使用AirPrint链接打印机打印)。详见点我。

UIPrintPageRenderer类提供了很多打印绘制的方法,一半情况下我们不需要重载这些方法。当然为了使打印内容有更灵活的掌控(例如添加页眉、页脚),我们可以在UIPrintPageRenderer子类中对这些方法进行重载。在文中最终的打印文档中会添加页眉、页脚,所以我们会新建一个UIPrintPageRenderer子类。

与之前的新建过程类似,不过需要注意以下两点:

新建的类继承自UIPrintPageRenderer

类名为CustomPrintPageRenderer

新建完成后,我们先来A4纸尺寸来初始化widthheight。请注意我们的目标是将发票导出为PDF文件,那么这个PDF文件也应该能够被打印机完美打印出来,所以定义尺寸是很重要的一件事。

class CustomPrintPageRenderer: UIPrintPageRenderer {
    let A4PageWidth: CGFloat = 595.2

    let A4PageHeight: CGFloat = 841.8

}

接下来我们在init()中使用这两个属性来指定CustomPrintPageRenderer的纸张大小和打印区域大小。

override init() {
    super.init()

    // Specify the frame of the A4 page.
    let pageFrame = CGRect(x: 0.0, y: 0.0, width: A4PageWidth, height: A4PageHeight)

    // Set the page frame.
    self.setValue(NSValue(CGRect: pageFrame), forKey: "paperRect")

    // Set the horizontal and vertical insets (that"s optional).
    self.setValue(NSValue(CGRect: pageFrame), forKey: "printableRect")
}

因为paperRectprintableRect都是只读属性,所以才会使用上面的方法来设置对应的属性值。

上面的代码中,纸张大小和打印区域大小是一样大的。也许你希望打印的时候能有一些边距,那么你可以将最后一行代码替换为:

setValue(NSValue(CGRect: CGRectInset(pageFrame, 10.0, 10.0)), forKey: "printableRect")

上面的代码在水平和垂直方向都设置了十个点的边距。上面的设置即使不是使用UIPrintPageRenderer子类也应该要配置。换句话说,只要使用UIPrintPageRenderer对象都都不能忘了设置打印配置。

打印为PDF

打印为PDF意味着需要将一些内容绘制为PDF文档,并将文档发送给打印机或者保存为文档。因为本文的关注点是导出文档,所有我们会保存绘制后的NSData对象,最后将该返回结果保存为PDF文件。下面我们一步步来实现:

首先在InvoiceComposer.swift文件中,实现一个名为exportHTMLContentToPDF(...)新函数,该函数将需要打印的内容HTMLContent作为唯一参数。但是在我们对该函数进行编码之前,我们有必要了解与打印相关的另一个概念:打印格式UIPrintFormatter。下面是官方文档中该类的描述:

UIPrintFormatter是打印格式的抽象基类。该类能够对打印内容进行布局,打印系统会自动将与打印格式绑定的内容打印出来。

这意味着:只需要简单的将打印的内容与打印格式绑定并传递给打印渲染器,iOS打印系统会完成后面的任务。建议你去该网页了解详情。简单来说,我们可以把打印格式理解为需要打印渲染器打印的内容。另外,虽然UIPrintFormatter是抽象类,iOS SDK还是提供了几个具体的子类。这里我们需要使用的就是打印标记语言内容的UIMarkupTextPrintFormatter,这些具体的打印格式类也可以在上面的链接中找到。

下面就是具体的实现代码:

func exportHTMLContentToPDF(HTMLContent: String) {
    let printPageRenderer = CustomPrintPageRenderer()

    let printFormatter = UIMarkupTextPrintFormatter(markupText: HTMLContent)    
    printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAtIndex: 0)

    let pdfData = drawPDFUsingPrintPageRenderer(printPageRenderer)

    pdfFilename = "(AppDelegate.getAppDelegate().getDocDir())/Invoice(invoiceNumber).pdf"
    pdfData.writeToFile(pdfFilename, atomically: true)

    print(pdfFilename)
}

注释如下:

首先创建CustomPrintPageRenderer类型实例。

接下来使用打印内容创建UIMarkupTextPrintFormatter类型实例。

printFormatter作为参数传给了printPageRendereraddPrintFormatter函数。该函数的第二个参数表示当前打印内容的起始页,这里默认为0。

使用紧接着会实现的自定义函数drawPDFUsingPrintPageRenderer得到待打印的NSData对象。

保存上一步的到的数据为PDF文件。

最后我们打印出该文件的路径。

在真实的复杂应用中,我们可能会需要为每一个起始页的打印内容自定义对应的打印格式,但是对于本文的Demo来说上面的代码够用了。

下面我们来实现是第四步中的自定义函数。在函数中我们使用了Core Graphics来实现PDF文件内容的绘制。整个函数的代码简短清晰:

func drawPDFUsingPrintPageRenderer(printPageRenderer: UIPrintPageRenderer) -> NSData! {
    let data = NSMutableData()

    UIGraphicsBeginPDFContextToData(data, CGRectZero, nil)

    UIGraphicsBeginPDFPage()

    printPageRenderer.drawPageAtIndex(0, inRect: UIGraphicsGetPDFContextBounds())

    UIGraphicsEndPDFContext()

    return data
}

首先创建了一个NSMutableData对象用于写入后面的输出,这也是开始创建文档前的前奏。然后就是创建新文档了,不过真正绘制部分的是下面的代码:

printPageRenderer.drawPageAtIndex(0, inRect: UIGraphicsGetPDFContextBounds())

该段代码完成了PDF文件上下文的绘制,并且自定义的页眉和页脚也会完成绘制。因为drawPageAtIndex函数会调用渲染器中的其他部分绘制方法。

最后我们关闭PDF文件的Graphics上下文,并将绘制的结果数据对象返回。

上面的代码只完成了单页文件的绘制,如果你要绘制多页文档的话可以将开始绘制、和真正绘制部分的代码放在一个循环结构里面。

到目前为止,与PDF文档绘制的任务都已经完成了。但是在后面还会实现自定义页眉和页脚的绘制。当然我们还需要在PreviewViewController.swift文件的exportToPDF中调用上面实现的功能函数:

@IBAction func exportToPDF(sender: AnyObject) {
    invoiceComposer.exportHTMLContentToPDF(HTMLContent)
}

现在我们可以来测试效果了,为了方便查看我建议使用模拟器。我们进入发票的预览界面后,点击右上角的导出PDF按键:

等创建文档任务完成后,我们可以在控制台看见该文件的路径。我们打开Finder窗口并使用Shift-Command-G定位到文件的父目录中你就可以你创建的PDF文件了:

双击新建的文件,你可以看见:

绘制自定义页眉、页脚

现在让我们来对打印结果做一些拓展,添加页眉和页脚。这也是为什么在前面我会自定义一个UIPrintPageRenderer类。我们所说的打印内容,除了使用HTML模版生成部分还包括页眉和页脚。我们会在右上角添加"Invoice"作为页眉、下方添加“Thank you!”作为页脚。最终效果如下图:

在了解实现细节之前,我们需要在CustomPrintPageRenderer类的init()函数中初始化页眉、页脚的高度:

override init() {
    ...

    self.headerHeight = 50.0
    self.footerHeight = 50.0
}

接下来我们重载UIPrintPageRenderer类中绘制页眉的函数:

override func drawHeaderForPageAtIndex(pageIndex: Int, inRect headerRect: CGRect) {

}

在函数体内我们实现的步骤如下:

初始化我们需要在页眉中绘制的"Invoice"。

初始化与text格式相关的属性值,例如字体、颜色、字间距。

计算页眉显示内容的显示区域大小,并设置与右边距。

计算绘制页眉的起始位置。

绘制页眉内容。

下面就是对应的代码,每一行都带有注释:

override func drawHeaderForPageAtIndex(pageIndex: Int, inRect headerRect: CGRect) {
    // Specify the header text.
    let headerText: NSString = "Invoice"

    // Set the desired font.
    let font = UIFont(name: "AmericanTypewriter-Bold", size: 30.0)

    // Specify some text attributes we want to apply to the header text.
    let textAttributes = [NSFontAttributeName: font!, NSForegroundColorAttributeName: UIColor(red: 243.0/255, green: 82.0/255.0, blue: 30.0/255.0, alpha: 1.0), NSKernAttributeName: 7.5]

    // Calculate the text size.
    let textSize = getTextSize(headerText as String, font: nil, textAttributes: textAttributes)

    // Determine the offset to the right side.
    let offsetX: CGFloat = 20.0

    // Specify the point that the text drawing should start from.
    let pointX = headerRect.size.width - textSize.width - offsetX
    let pointY = headerRect.size.height/2 - textSize.height/2

    // Draw the header text.
    headerText.drawAtPoint(CGPointMake(pointX, pointY), withAttributes: textAttributes)
}

上面的代码中惟一需要注意的就是函数getTextSize(...)。在该函数会计算显示内容的大小,因为后面打印页脚的时候也需要使用所以就抽离出来了。代码如下:

func getTextSize(text: String, font: UIFont!, textAttributes: [String: AnyObject]! = nil) -> CGSize {
    let testLabel = UILabel(frame: CGRectMake(0.0, 0.0, self.paperRect.size.width, footerHeight))
    if let attributes = textAttributes {
        testLabel.attributedText = NSAttributedString(string: text, attributes: attributes)
    }
    else {
        testLabel.text = text
        testLabel.font = font!
    }

    testLabel.sizeToFit()

    return testLabel.frame.size
}

上面代码是计算text文本size大小的通用方法。先创建一个UILabel对象,设置简单文本的字体或者attributedText属性之后使用sizeToFit()方法让系统来计算真实的size。

页脚部分的处理和上面类似,并没有什么太多需要额外讲的。惟一需要注意的是页脚的位置是水平居中、字体颜色也与页眉存在差异,还有就是字母之间没有间距。

    ovrride func drawFooterForPageAtIndex(pageIndex: Int, inRect footerRect: CGRect)      {
    let footerText: NSString = "Thank you!"

    let font = UIFont(name: "Noteworthy-Bold", size: 14.0)
    let textSize = getTextSize(footerText as String, font: font!)

    let centerX = footerRect.size.width/2 - textSize.width/2
    let centerY = footerRect.origin.y + self.footerHeight/2 - textSize.height/2
    let attributes = [NSFontAttributeName: font!, NSForegroundColorAttributeName: UIColor(red: 205.0/255.0, green: 205.0/255.0, blue: 205.0/255, alpha: 1.0)]

    footerText.drawAtPoint(CGPointMake(centerX, centerY), withAttributes: attributes)
}

页脚已经正确显示了,下面我们补上页脚上面的水平线:

ovrride func drawFooterForPageAtIndex(pageIndex: Int, inRect footerRect: CGRect) {
    ...

    // Draw a horizontal line.
    let lineOffsetX: CGFloat = 20.0
    let context = UIGraphicsGetCurrentContext()
    CGContextSetRGBStrokeColor(context, 205.0/255.0, 205.0/255.0, 205.0/255, 1.0)
    CGContextMoveToPoint(context, lineOffsetX, footerRect.origin.y)
    CGContextAddLineToPoint(context, footerRect.size.width - lineOffsetX, footerRect.origin.y)
    CGContextStrokePath(context)
}

在结束这一部分内容之前,关于页眉、页脚的处理有一个小细节需要跟大家说一下。如果你足够细心的话,你会发现函数中使用了NSString而不是String来处理页眉、页脚。之所以这么做是因为:处理文本绘制的函数drawAtPoint(...)属于NSString类,如果你使用String的话则需要进行类型转换:

(text as! NSString).drawAtPoint(...)

再次运行程序你就可以看见带页眉、页脚的PDF了。

附赠部分:预览并Email发送PDF文档

文中到了这里其实主要的内容已经讲解完了。然而,在设备中运行Demo的时候我们没有什么方法直接查看导出的PDF文档(除了每次创建新文档的时候通过XCode去找文档路径)。所以最后这部分提供两种可选的方法:使用PreviewViewController中的webview视图预览PDF文档;使用Email将PDF文档发送出去。我们会弹出一个提示窗口让用户自己选择最终的处理。该部分代码已经超出了文章的内容,所以不会有太多的细节。实现代码如下(PreviewViewController.swift文件中):

func showOptionsAlert() {
    let alertController = UIAlertController(title: "Yeah!", message: "Your invoice has been successfully printed to a PDF file.

What do you want to do now?", preferredStyle: UIAlertControllerStyle.Alert)

    let actionPreview = UIAlertAction(title: "Preview it", style: UIAlertActionStyle.Default) { (action) in

}

    let actionEmail = UIAlertAction(title: "Send by Email", style: UIAlertActionStyle.Default) { (action) in

}

    let actionNothing = UIAlertAction(title: "Nothing", style: UIAlertActionStyle.Default) { (action) in

}

    alertController.addAction(actionPreview)
    alertController.addAction(actionEmail)
    alertController.addAction(actionNothing)

    presentViewController(alertController, animated: true, completion: nil)
}

下面来实现不同选项对应的动作。针对预览操作,我们使用NSURLRequest对象来实现webview中对内容的加载和显示:

let actionPreview = UIAlertAction(title: "Preview it", style: UIAlertActionStyle.Default) { (action) in
    let request = NSURLRequest(URL: NSURL(string: self.invoiceComposer.pdfFilename)!)
    self.webPreview.loadRequest(request)
}

对于Email发送的功能,我们会创建一个新的函数并将PDF文件作为Eamil的附件:

func sendEmail() {
    if MFMailComposeViewController.canSendMail() {
        let mailComposeViewController = MFMailComposeViewController()
        mailComposeViewController.setSubject("Invoice")
        mailComposeViewController.addAttachmentData(NSData(contentsOfFile: invoiceComposer.pdfFilename)!, mimeType: "application/pdf", fileName: "Invoice")
        presentViewController(mailComposeViewController, animated: true, completion: nil)
    }
}

为了正常使用MFMailComposeViewController,我们需要在文件中加上:

import MessageUI

回到函数showOptionsAlert()中,补全actionPreview动作中的代码:

let actionEmail = UIAlertAction(title: "Send by Email", style: UIAlertActionStyle.Default) { (action) in
    dispatch_async(dispatch_get_main_queue(), {
    self.sendEmail()
    })
}

函数代码都已经写好了,剩下的就是在合适的地方调用了。调用的时机很明显就是当我们点击右上角按键创建PDF文档的时候,所以代码如下:

@IBAction func exportToPDF(sender: AnyObject) {
    ...

    showOptionsAlert()
}

一切就绪,现在你可以预览文档并通过Email发送了:

总结

对于创建PDF而言,无论现在的其他方案或者以后的新技巧,本文所提及的解决方案总会是标准、灵活和安全的之一。该方案惟一的缺点就是:我们需要编写那些HTML模版文件。不过对于我来说,这工作实在是物超所值。与花大量工作去手动绘制PDF相比,我坚信替换模版文件中的“占位符”的做法更加可取。除此之外,真实情况中的PDF文档绘制都是非常标准的,只需要对Demo中的代码进行部分调整就能实现复用了。不管怎样,我都希望本文中的方法能够真正的帮到你。

本文的完整Demo代码地址,仅供读者参考。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/16396.html

相关文章

  • UIPrint​Interaction​Controller

    原文链接:http://nshipster.com/uiprintinteractioncontroller/ 前言 UIKit使用户设备直接打印变得方便起来,而且打印可以根据内容或者纸张大小进行自定义排版。这篇文章的意义在于让你明白在打印的时候如何格式化你的内容,详细阐述不同的方式来展示打印接口。 Xcode6中,打印机模拟器作为硬件IO工具属于其的一部分。 showImg(ht...

    张春雷 评论0 收藏0
  • 我是如何将博客转成PDF

    摘要:但发现导出来的没有高亮语法没有语法高亮咋看啊,所以到这里我就放弃了,将就用一下博客园生成的吧爬虫学习上面提供的接口是一个生成一个文件,我是不可能一个一个将链接和标题放上去生成的因为博客园上发的也将近篇了。 前言 只有光头才能变强 之前有读者问过我:3y你的博客有没有电子版的呀?我想要份电子版的。我说:没有啊,我没有弄过电子版的,我这边有个文章导航页面,你可以去文章导航去找来看呀..然后...

    mindwind 评论0 收藏0
  • Laravel Excel 的五个隐藏功能

    摘要:文档示例注意你必须通过安装指定的包,比如导出的如下所示按需格式化单元格有一个强有力的爸爸。因此它就拥有其各种底层功能,包括各种方式的单元格格式化。 showImg(https://segmentfault.com/img/remote/1460000018375787?w=1680&h=859); Laravel Excel package 最近发布了 3.0 版本,它所具有的新功能,...

    tinna 评论0 收藏0
  • Ionic2入坑基础教程安装指南

    摘要:安装程序主要通过命令行工具来创建和开发,并使用来构建和部署为原生应用程序。基础教程确保完成之前的安装并测试启动成功。 安装Ionic Ionic 2 程序主要通过Ionic命令行工具CLI来创建和开发,并使用Cordova来构建和部署为原生应用程序。也就是说我们需要先安装一些工具来实现程序开发。 安装Ionic CLI 和 Cordova 要创建 Ionic 2 项目,你需要安装最新版...

    jayce 评论0 收藏0
  • Web应用程序如何创建 PDF

    摘要:在本文中,将探讨如何从一个应用程序中直接生成一个。然而,再次受到浏览器对和支持的限制。使用来实现规范尝试使用实现规范实际上是创建了。就从应用程序使用这些工具而言,需要在服务器上安装它们。希望这是一个有用的工具总结,可用帮你的应用程序创建。 为了保证的可读性,本文采用意译而非直译。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 在一些场景下,用户都要求一些需要的数...

    AlexTuan 评论0 收藏0

发表评论

0条评论

dreamtecher

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<