[iOS] 如何使用 HTML 模版和 iOS 中的 UIPrintPageRenderer 來生成 PDF 文檔

zzit0721 8年前發布 | 40K 次閱讀 HTML iOS開發 移動開發

作者:GABRIEL THEODOROPOULOS,時間:2016/7/10

翻譯:BigNerdCoding, 如有錯誤歡迎指出。 原文鏈接

你是否曾經被要求過在app中直接將內容生成為PDF文檔?如果沒有的話,你是否思考過這個需求該如何實現呢?

雖然使用提問的方式作為文章開頭有點不按套路出牌,但是這些問題就是本文要討論的重點。在app中創建PDF文檔,看起來就是一條布滿坑的路,但是事實上可能并沒有那么恐怖。作為開發者,在面對困難的時候我們總是需要一些替換方案,避免一條道走到黑。手動生成PDF頁面確實是一個非常痛苦的過程(取決于文檔的內容)并且最終可能會是事倍功半的結果。計算位置、添加線、配色、插入、偏移等等,可能有趣(也可能沒有)。但是如果文檔內容復雜的話,那么肯定是一件坑爹的事。不太可能有人喜歡干這樣的事。

在本文中我會給你介紹一種新思路來創建PDF文檔,并且比手動繪制要簡單不少。處理方法是基于使用 HTML templates ,并且可以概括為以下幾步:

  1. 為那些需要打印為PDF的表單或者內容創建 HTML templates
  2. 使用上面的 HTML templates 來生成真實的內容(可以在web view中進行預覽)
  3. 將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模版文件中設置一些“占位符”,然后將需要展示的信息替換這些“占位符”。為了實現這一目的首先就是要創建符合展示效果的自定義模版。但是本文的關注點并不是這個,所以我們會使用一個已有的模版 地址 (感謝原作者)。本文已經對模版做了一些修改,去除了邊界和陰影并給logo添加了灰色背景。

在你下載的起始工程里面,你可以看見下面三個HTML模版文件:

  1. invoice.html
  2. last_item.html
  3. 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
}

注意: getAppDelegategetStringValueFormattedAsCurrency 方法的具體實現,我已經在前面提過了。它們都在 AppDelegate.swift 文件中。

這一步到這里就結束了,我們成功實現了真實發票HTML格式信息的生成。接下來就是對該結果的進一步處理了。

預覽處理后的HTML內容

在上一步處理完成后,接下來就需要驗證結果是否正確了。因此這一部分內容的目的就是使用 PreviewViewController 視圖中的webview來加載該HTML內容,查看我們前面努力的效果。需要注意的是:在真實的應用中這一步是可選的,我們可以跳過預覽直接打印PDF,這里之所以需要預覽僅僅是為了Demo的功能完整性而已。

我們在 PreviewViewController.swift 文件中聲明屬性:

class PreviewViewController: UIViewController {

    ...

    var invoiceComposer: InvoiceComposer!

    var HTMLContent: String!

}

第一個屬性就是新建的類的實例,而 HTMLContent 屬性則是對應最終內容的 String 類型變量我們會在后面用到它。

接下來我們創建一個函數來實現如下功能:

  1. 初始化 invoiceComposer 對象
  2. 調用 invoiceComposer 對象的 renderInvoice(...) 函數得到發票的HTML編碼內容
  3. 在webview中加載該內容
  4. 將得到的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 子類。

與之前的新建過程類似,不過需要注意以下兩點:

  1. 新建的類繼承自 UIPrintPageRenderer
  2. 類名為 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)
}

注釋如下:

  1. 首先創建 CustomPrintPageRenderer 類型實例。
  2. 接下來使用打印內容創建 UIMarkupTextPrintFormatter 類型實例。
  3. printFormatter 作為參數傳給了 printPageRendereraddPrintFormatter 函數。該函數的第二個參數表示當前打印內容的起始頁,這里默認為0。
  4. 使用緊接著會實現的自定義函數 drawPDFUsingPrintPageRenderer 得到待打印的 NSData 對象。
  5. 保存上一步的到的數據為PDF文件。
  6. 最后我們打印出該文件的路徑。

在真實的復雜應用中,我們可能會需要為每一個起始頁的打印內容自定義對應的打印格式,但是對于本文的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) {

}

在函數體內我們實現的步驟如下:

  1. 初始化我們需要在頁眉中繪制的"Invoice"。
  2. 初始化與text格式相關的屬性值,例如字體、顏色、字間距。
  3. 計算頁眉顯示內容的顯示區域大小,并設置與右邊距。
  4. 計算繪制頁眉的起始位置。
  5. 繪制頁眉內容。

下面就是對應的代碼,每一行都帶有注釋:

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://www.jianshu.com/p/8b3197f90c64

 

Save

 本文由用戶 zzit0721 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!