如何使用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("\n", 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..<items.count {

var itemHTMLContent: String!

// Determine the proper template file.

if i != items.count - 1 {

itemHTMLContent = try String(contentsOfFile: pathToSingleItemHTMLTemplate!)

}

else {

itemHTMLContent = try String(contentsOfFile: pathToLastItemHTMLTemplate!)

}

// Replace the description and price placeholders with the actual values.

itemHTMLContent = itemHTMLContent.stringByReplacingOccurrencesOfString("#ITEM_DESC#", withString: items[i]["item"]!)

// Format each item's price as a currency value.

let formattedPrice = AppDelegate.getAppDelegate().getStringValueFormattedAsCurrency(items[i]["price"]!)

itemHTMLContent = itemHTMLContent.stringByReplacingOccurrencesOfString("#PRICE#", withString: formattedPrice)

// Add the item's HTML code to the general items string.

allItems += itemHTMLContent

}

//Set the items.

HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#ITEMS#", withString: allItems)

// The HTML code is ready.

return HTMLContent

}

catch {

print("Unable to open and use HTML template files.")

}

return nil

}

注意:getAppDelegate和getStringValueFormattedAsCurrency方法的具体实现,我已经在前面提过了。它们都在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纸尺寸来初始化width和height。请注意我们的目标是将发票导出为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")

}

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

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

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作为参数传给了printPageRenderer的addPrintFormatter函数。该函数的第二个参数表示当前打印内容的起始页,这里默认为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.\n\nWhat 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代码地址,仅供读者参考。

ios html格式转换,如何使用HTML模版和iOS中的UIPrintPageRenderer来生成PDF文档相关推荐

  1. java xsl转换pdf_Java 生成PDF文档-阿里云开发者社区

    最近项目需要实现PDF下载的功能,由于没有这方面的经验,从网上花了很长时间才找到相关的资料.整理之后,发现有如下几个框架可以实现这个功能. 1. 开源框架支持 iText,生成PDF文档,还支持将XM ...

  2. 安卓实现文本以pdf格式保存,导出时中文出现的空白问题,以及打印pdf文档

    1.实现以pdf格式保存时,需要下载iTextpdf.jar,导入项目中: 下载链接:http://www.pc6.com/softview/SoftView_438379.html 2.在Andro ...

  3. Python 技术篇-利用pdfkit库实现html格式文件转换PDF文档实例演示

    准备: 首先需要安装 pdfkit 库,使用 pip install pdfkit 命令就好了. 还需要安装 wkhtmltopdf 工具,本质就是利用这个工具来进行转换,pdfkit 库就是作为接口 ...

  4. java将office文档,word,ppt,pdf文档转换成swf文件在线预览

    java将office文档pdf文档转换成swf文件在线预览 第一步,安装openoffice.org openoffice.org是一套sun的开源office办公套件,能在widows,linux ...

  5. java将office文档pdf文档转换成swf文件在线预览

    第一步,安装openoffice.org openoffice.org是一套sun的开源office办公套件,能在widows,linux,solaris等操作系统上执行. 主要模块有writer(文 ...

  6. 用PDF格式协议的方式给PDF文档添加透明文字水印

    用PDF格式协议的方式给PDF文档添加透明文字水印 *有情提示:本文适合有一定pdf格式协议基础的人阅读. 一.生成一个HelloWorld.pdf 首先,为了方便介绍,我们先写一个Helloworl ...

  7. 如何把PDF文档转换成TXT格式文件

    在日常学习和日常工作中,如何将PDF文件转换为TXT文本? 尽管PDF文件的用户数量正在增加,但是TXT文本格式的接受度将会更高. 毕竟,TXT文本格式是任何设备都可以打开的格式.那么如何将PDF文件 ...

  8. 使用poi根据模版生成word文档并转换成PDF文件(可处理doc文件与docx文件版)

    该篇文章是<使用poi根据模版生成word文档并转换成PDF文件>后续解决传入文件为doc文档或docx的处理方法 /*** 根据模板生成word** @param path 模板的路径* ...

  9. 使用poi根据模版生成word文档并转换成PDF文件

    一.首先制作word模版(这里需要注意的是文件后缀是docx不能是doc),${xxxx}是一会要替换的内容 关于为何必须是docx后缀可以看这篇文章https://www.cnblogs.com/c ...

最新文章

  1. C++ 32位和64位
  2. python程序员一天写多少行代码-这个工具,30分钟居然把我一天的工作给干完了!...
  3. Android应用插件式开发解决方法[转]
  4. Qt笔记-递归获取文件夹中文件(含过滤文件名)
  5. 枚举+贪心--经常用到的思路--过程不好弄是--枚举结果C. Elections
  6. pythonwin下载中文版_Python官方下载 v3.9.0中文版_Win10镜像官网
  7. MATLAB使用cic滤波器,基于Matlab的CIC滤波器设计
  8. 51单片机小车—循迹温湿度检测显示
  9. 错误代码:88000, 错误信息:without comment privilege hint: [7oJ0533w689] rid: 630432cd-15944cf6-083e04fc
  10. windows10系统如何设置开机自启动
  11. Photoshop快速复制图层的几种方法和技巧
  12. python爬虫教程:实例讲解Python爬取网页数据
  13. 分享几个比较通用的学习网站
  14. html网页播放flac,教你如何无损录制网页上播放的声音
  15. Web技术的发展 网络发展简介(三)
  16. java号码分身_电话号码分身问题
  17. 开发者出海跨境收款——如何完美解决限额问题?
  18. 奇迹mu最新服务器端,奇迹 开区必备服务端 1.03H 最新特色
  19. 我整理了三百多篇论文,分享写文献综述的经验
  20. js禁止退格键(Backspace)

热门文章

  1. 计算机关机重启后黑屏,电脑重启黑屏强制关机后才能开怎么办
  2. linux设置mysql防火墙端口映射_Linux防火墙默认是关闭3306端口,iptables实现端口转发、端口映射及双向通路...
  3. U盘安装Ubuntu16.04报unable to find a medium containing a live file system和ACPI Error错误
  4. Word中插入多张图片/论文图片排版的方法
  5. ZYNQ PS GPIO MIO 基础知识
  6. latex如何更改某一段落的字体_LaTeX 设置字体
  7. Java就业方向和自学提升方法总结黑马就业班资源分享
  8. 修改MP4文件二进制内容,实现安卓Camera2旋转录制视频画面功能
  9. 如何用老毛挑清除WINDOWS密码(以HP笔记本为例)
  10. Tiled结合Unity实现瓦片地图