如何使用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<br>
#INVOICE_DATE#<br>
#DUE_DATE# </td>
注意:虽然在模版中有失效日期的“占位符”,但在文中我们并不会真的用到。我们会使用一个空字符串来替换这个“占位符”,当然如果你想使用也没有任何问题。
你可以在三个模版文件中找到所有的“占位符”以及它们的位置。下面列出全部的“占位符”:
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<br>123 Somewhere Str.<br>10000 - MyCity<br>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: "<br>"))
// 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代码地址,仅供读者参考。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。