HTML5中国

 找回密码
 立即注册

QQ登录

只需一步,快速开始

HTML5中国 首页 移动互联网 查看内容

在 iOS 中使用 HTML 模版和 UIPrintPageRenderer 生成 PDF(下)

2016-9-7 09:49| 发布者: Hyukoh| 查看: 1077| 评论: 0|原作者: SwiftGG翻译组|来自: SegmentFault

摘要: 你是否曾经遇到过「使用 app 中的内容生成 PDF 文件」这样的需求?如果你之前没有做过,那你有想过该如何实现吗?

  输出为 PDF


  说是「输出为 PDF」,其实是把内容绘制到一个 PDF 图形的上下文。一旦绘制完成,完成好的内容可以发送到打印机打印,也可以被保存成一个文件。我们对第二种情况比较感兴趣,所以我们会把绘制好的 PDF 上下文转换成 NSData 对象,然后把这个对象保存到文件中(最终的 .pdf 文件)。让我们来一步一步进行。


  先打开 InvoiceComposer.swift 文件,在这里我们要实现一个新的方法 exportHTMLContentToPDF(...)。它只接受一个参数,我们想要输出到 PDF 的 HTML 内容。在看这个方法的实现之前,我们先来看看另一个跟打印相关的概念,也就是 print formatter (UIPrintFormatter class)。下面是 Apple 文档对它的介绍:


  UIPrintFormatter is an abstract base class for print formatters: objects that lay out custom printable content that can cross page boundaries. Given a print formatter, the printing system can automate the printing of the type of content associated with the print formatter.


  这意味着我们只需把 HTML 内容作为打印的 formatter 添加到打印的 renderer,iOS 打印系统将接管页面布局和实际的打印页面。我建议你看一看这里,有详细的解释。简单来说,就是把 print formatter 想要打印的内容传递给 iOS 打印系统的一种中介。此外,虽然UIPrintFormatter 是一个抽象类,但 iOS 的 SDK 提供了有实现的子类来给我们使用。其中之一是 UIMarkupTextPrintFormatter,我们可以用它把 HTML 内容转换成 page renderer 对象。还有一些其它的子类信息可以在上面的链接中找到。


  光说还是有些不清楚,看看代码吧:

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 对象,在初始化的时候,我们把 HTML content 作为参数传了进去。
  •   第三行,把 printFormatter 加到了 printPageRenderer 对象中。addPrintFormatter(...) 方法的第二个参数是指定 printFormatter 起始生效的页面。我们在这里设置为 0,因为打印的内容只有一页。
  •   真正的绘制即将发生。drawPDFUsingPrintPageRenderer(...) 是一个我们在后面才会创建的自定义方法。绘制完成的 PDF 会被存放在 pdfData 对象中,它实际上是一个 NSData 类型的对象。
  •   接下来就是把 PDF 数据存入文件。首先我们声明了文件路径,以**的号码来指定文件名。然后把 PDF 数据写入这个文件中。
  •   最后一步显然不是必要的,但是我们可以通过在 Finder 中找到这个新创建的文件,来验证我们绘制的结果。

  在一个更复杂的应用中,你可以使用多个 print formatter 对象,当然也可以对不同的 print formatter 指定不同的起始页面。但是对于我们来说,创建一个对象能够说明问题就足够了。


  现在我们来把上面没有实现的,也就是真正绘制的方法给实现了。在这里我们使用了 Core Graphics,下面的方法也很直白,一起来看看吧:

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

    UIGraphicsBeginPDFContextToData(data, CGRectZero, nil)

    UIGraphicsBeginPDFPage()

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

    UIGraphicsEndPDFContext()

    return data
}

  首先我们初始化了一个 NSMutableData 对象,是用来写入 PDF 数据的。然后我们创建了 PDF 图形上下文来开始 PDF 绘制。接下来才是绘制的代码:

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

  作为参数的 printPageRenderer 对象在这一行开始了绘制工作,它会把内容绘制在 PDF 上下文的区域中。注意,在这里自定义的 header 和 footer 也会被自动绘制,因为 drawPageAtIndex(...) 调用了 printPageRenderer 对象中所有的绘制方法。


  最后我们关闭了 PDF 图形上下文,然后返回了 data 对象。


  上面的方法只能打印一个单页面,如果你想要打印多个页面,或者你想要扩展这个 demo 应用,可以把上面的操作放到一个循环中。


  到此为止,所有关于 PDF 输出的部分就已经结束了,但是我们的工作还没有结束,在下一部分我们会绘制 header 和 footer。不过在那之前,我们先把上面的工作串联起来。


  打开 PreviewViewController.swift 文件,定位到 exportToPDF(...) IBAction 方法。把下面几行加进去。点击按钮的时候就可以把**导出为 PDF 文件了。

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

  你现在就可以测试应用了,但是为了快速看到结果,我建议你在模拟器中进行下面的操作。在预览**界面,点击 PDF 按钮:

  之后,输出 PDF 这个过程就已经发生了,当一切都结束的时候,你将会在控制台看到 PDF 文件的路径。把路径复制一下(不要带上文件名),打开一个 Finder 窗口,使用 Shift-Command-G 快捷键,粘贴上路径,在打开的文件夹中你就可以看到以**号码为名字的新创建的 PDF 文件。

  双击打开它,用你喜欢的 PDF 程序就好。

  绘制自定义的 Header 和 Footer


  现在扩展一下我们的 demo,往打印页面添加自定义的 header 和 footer。毕竟这也是我们最初子类化 UIPrintPageRenderer 的原因。自定义的意思是,不是 HTML 模板中的一部分,不是和其它的 HTML 内容一起渲染的内容。我们想要实现的是把 「Invoice」放在页面的顶部,作为 header,把「Thank you!」放在页面的底部,作为页面的 footer,在它上面还有一条水平线。下面的这张图就是我们要达到的效果:

  在开始之前,我们先声明一下 header 和 footer 的高度。打开 CustomPrintPageRenderer.swift 文件,添加下面两行(这两个属性都是继承自 UIPrintPageRenderer 的)。

override init() {
    ...

    self.headerHeight = 50.0
    self.footerHeight = 50.0
}

  我们先从 header 做起。先重写一下父类中的下面这个方法:

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

}

  在这个方法中我们要做的事情步骤如下所示:

  1.   首先指定我们要绘制的 header 文字(也就是「Invoice」单词)。
  2.   指定 header 文字的一些属性,比如字体、颜色、字间距等。
  3.   计算字在加上上述属性后占据的空间,然后指定文字到页面右侧页面的边距。
  4.   设置文字起始绘制的点。
  5.   绘制文字(终于到这一步了)。

  下面就是我上面文字转化为代码的实现。每句都有注释,方便大家理解:

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

    // 声明 header 文字。
    let headerText: NSString = "Invoice"

    // 设置字体。
    let font = UIFont(name: "AmericanTypewriter-Bold", size: 30.0)

    // 设置字的属性。
    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]

    // 计算字的大小。
    let textSize = getTextSize(headerText as String, font: nil, textAttributes: textAttributes)

    // 右边的空距。
    let offsetX: CGFloat = 20.0

    // 指定字应该从哪里开始绘制。
    let pointX = headerRect.size.width - textSize.width - offsetX
    let pointY = headerRect.size.height/2 - textSize.height/2

    // 绘制 header 的文字。
    headerText.drawAtPoint(CGPointMake(pointX, pointY), withAttributes: textAttributes)
}

  还有一件事我没有在上面的代码里说明的就是 getTextSize(...) 方法。跟你猜的一样,这又是另一个自定义方法,用于计算并返回文字的 frame。计算发生在另一个方法中,因为在绘制 footer 的时候也会用到这个方法。

  下面就是 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
}

  上面的方法对于计算文字占据的 frame 尺寸是一个通用的策略。我们把 textAttributes 设置到这个临时的 label 上。通过对其调用sizeToFit() 方法,让系统帮助我们计算这个 label 的尺寸。

  现在我们开始绘制 footer。下面的步骤跟上面绘制 header 的步骤十分相似,所以我也就没注释下面的代码。注意,footer 中的文字是水平居中的,文字颜色也和之前的不一样,字母之间也没有间距:

override 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)

}

  上述代码创建了「Thank you!」的 footer,但是在它上面还没有一条分隔线。因此,我们再把上面的方法补充一下:

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

    // 绘制水平线

    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)

}

  现在我们已经有了一条水平线!

  在这部分结束之前,关于 header 和 footer 还有几句话想说。不知你注意到了没有,header 和 footer 中的文字都是 NSString 对象而不是 String 对象,这是因为执行真正绘制的 drawAtPoint(...) 方法属于 NSString 类。如果你使用了 String 对象,那通过下面的方式把它转换成 NSString 的对象:

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

  运行应用然后检查一下结果,这一次已经包含了 header 和 footer。


  Bonus Part:预览并使用 Email 发送 PDF 文件


  到此为止,我们已经完成了这篇教程的主要目的。然而,当你在真机上运行时,没办法直接看到导出的 PDF 文件(你可以用 Xcode 查看,但是每次创建 PDF 都这么做就太麻烦了),所以我要给这个 app 增加两个额外的功能:在 web view 中预览 PDF 的功能(已在PreviewViewController 中实现),还有通过 Email 发送 PDF 文件的功能。我们可以显示一个有各种选项的 alert controller 来让用户做出最终选择。这里不会讲得太细,因为下面的代码已经超出了这篇教程的范围。


  我们会把代码写在 PreviewViewController.swift 文件中,所以在 Project Navigator 找到并打开它。加入以下显示 alert controller 的方法:

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)

}

  每个选项的 action 还没有被实现,所以我们现在开始实现。对于预览动作,我们通过 NSURLRequest 对象把 PDF 文件载入到 web view 中:

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

    let request = NSURLRequest(URL: NSURL(string: self.invoiceComposer.pdfFilename)!)

    self.webPreview.loadRequest(request)

}

  对于发送邮件,可以按照下面的方法来实现:

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,你还需要引入 MessageUI

import MessageUI

  回到 showOptionsAlert() 方法,按下面的代码段完成 actionPreview action:

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

    dispatch_async(dispatch_get_main_queue(), {

        self.sendEmail()

    })
}

  还差一点就完成了,别忘了我们还得调用 showOptionsAlert() 方法。Alert controller 会在**被输出为 PDF 文件之后出现,回到exportToPDF(...) IBAction 方法,加上下面的一句话:

@IBAction func exportToPDF(sender: AnyObject) {

    ...

    showOptionsAlert()
}

  完成!现在你可以在真机上运行这个应用并且使用导出的 PDF 文件了。

  总结

  不管现在还是以后在创建 PDF 文档方面出现了什么新的技术,本文展现的这个方法在创建 PDF 文件方面永远会是基本的、高灵活性的,且安全的。它适用于几乎所有的情形,但只有一个缺点:要用到 HTML 模板来渲染真正的内容 。但我认为,创建它的成本真得很低。相比于写 HTML、创建 placeholder、替换字符串来说,手动绘制 PDF 文件真得是太麻烦了。除此之外,真正绘制 PDF 部分的代码是很基本的,并且通过 demo 应用的代码,你可以获得很理想的结果。不管怎样,我希望你能喜欢本文中介绍的这种方法。感谢阅读!希望你能开心地处理输出 PDF 文档的问题!

  你可以在 Github.com 获取本文的 Xcode 项目 作为参考。


原文链接:https://segmentfault.com/a/1190000006824182

来源作者:SwiftGG翻译组

本站文章均由 HTML5中国 编辑从其他媒体精选HTML5相关文章转载,仅供网友学习和交流,如果我们的工作有侵犯到您的权益,请及时联系站长QQ:2601929995,我们会在第一时间进行处理!投稿: admin@html5cn.org
更多

鲜花

握手

雷人

路过

鸡蛋

相关阅读

最新评论

HTML5中国微信

小黑屋|关于我们|HTML5论坛|友情链接|手机版|HTML5中国 ( 京ICP备11006447号 京公网安备:11010802018489号  

GMT+8, 2017-4-27 05:31

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

返回顶部