2016年7月4日 星期一

[iOS] 產生PDF檔


  • Swift版本:2.1.1(在Terminal輸入$ xcrun swift -version指令可查看版本)
  • iOS版本:9.2
  • 模擬器環境

前不久工作需要APP有產生PDF報表檔、讀取及印出的功能,研究出來後在此做個記錄。
製作這些功能主要有以下幾個步驟:

產生PDF檔

  1. 使用UIGraphicsBeginPDFContextToFile產生一PDF內容
  2. 使用UIGraphicsBeginPDFPageWithInfo產生新的一頁
  3. 使用UIKit和Core Graphics繪製PDF內容
  4. 完成內容繪製後,使用UIGraphicsEndPDFContext結束

讀取PDF檔

  1. 建立UIWebView
  2. 取得檔案的URL後,使用loadRequest載入

印出、郵寄PDF檔

  1. 使用Quick Look Framework

詳細程式內容如下。


產生PDF檔

1. 使用UIGraphicsBeginPDFContextToFile產生一PDF內容

let pathArr:NSArray = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let path = pathArr[0]
let fileName = path.stringByAppendingPathComponent("mypdf.pdf")

為了安全性,iOS使用沙盒(Sand Box)機制去限制每個APP只能讀取為該APP建立的檔案系統。在iOS的檔案系統底下有個Documents資料夾用來存放使用者產生的檔案,而這個資料夾的路徑可用NSSearchPathForDirectoriesInDomains取得。該函數會回傳一個String型別的陣列存放路徑值,在給予的參數條件下,此陣列的長度為1。取陣列的第一個元素得到路徑值後,使用stringByAppendingPathComponent建立檔案名稱。接著使用UIGraphicsBeginPDFContextToFile在該檔案中建立PDF內容。

UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil)

2. 使用UIGraphicsBeginPDFPageWithInfo產生新的一頁

UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 792, 612), nil)

在開始繪製內容前,須呼叫UIGraphicsBeginPDFPageWithInfo函數來標記為新的一頁。CGRectMake(0, 0, 792, 612)分別為該頁內容的起點x,y及寬、高。792x612這個尺寸是US Letter尺寸,如要換為A4尺寸則是842x595像素(橫向)。

3. 使用UIKit和Core Graphics繪製PDF內容

iOS的2D繪圖基於UIKit及Core Graphics兩個框架來處理,首先以文字為例。
let myText:NSString = "Nothing in all the world is more dangerous than sincere ignorance and conscientious stupidity."
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
textStyle.alignment = NSTextAlignment.Center
let textFontAttributes = [
    NSFontAttributeName: UIFont.boldSystemFontOfSize(12),
    NSForegroundColorAttributeName: UIColor.blueColor(),
    NSParagraphStyleAttributeName: textStyle
]
myText.drawInRect(CGRectMake(0, 0, 792, 612), withAttributes: textFontAttributes)

第一行宣告欲繪製的文字變數myText(型別須為NSString)後,接下來設定文字樣式屬性。建立一個NSMutableParagraphStyle,設定alignment為center(文字置中對齊),並建立一Dictionary放置文字大小、顏色及對齊的樣式。NSFontAttributeName為設定文字字體,NSForegroundColorAttributeName設定文字顏色(NSString所包含的樣式屬性列表)。最後就是呼叫drawInRect函數,帶入繪圖區域及樣式設定的參數,進行NSString的繪製。

除了文字以外,可能還有繪製線條的需求。由於線條的繪製很常用到,所以建立一個drawLine函數供使用,帶有兩個參數值,分別是線條的起始y位置(y),以及線條寬度(lineWidth)。在這個函數中,首先使用UIGraphicsBeginImageContextWithOptions函數建立一個圖形的上下文(context),並建立一個變數context儲存由UIGraphicsGetCurrentContext回傳的目前上下文。接下來建立一個img變數,儲存UIGraphicsGetImageFromCurrentImageContext函數回傳由現在圖形上下文內容所轉成的圖片(UIImage型別),並用UIGraphicsEndImageContext函數從堆疊頂端移出目前的圖形上下文。最後使用drawInRect將這個圖片繪製上去。

func drawLine(y:CGFloat,lineWidth:CGFloat) -> Bool{
     UIGraphicsBeginImageContextWithOptions(CGSize(width: 792, height: 612), false, 0)
     let context = UIGraphicsGetCurrentContext()
        
     CGContextMoveToPoint(context, 0, 0)//移至繪製點(0,0)
     CGContextAddLineToPoint(context, 792, 0)//新增一條線至座標(792,0)
     CGContextSetLineWidth(context, lineWidth)//設定線條寬度
     CGContextSetStrokeColorWithColor(context, UIColor.blackColor().CGColor)//設定線條顏色
     CGContextStrokePath(context)//開始繪製線條
        
     let img=UIGraphicsGetImageFromCurrentImageContext()
     UIGraphicsEndImageContext()
     img.drawInRect(CGRectMake(0, y, 792, 612))
     return true
}

每要繪製線條時,就呼叫drawLine(y:CGFloat,lineWidth:CGFloat)。由於欲在上面那串文字的下方再呈現一條線,因而需要取得文字的高度。這邊可使用NSString的sizeWithAttributes函數取得(上面文字的大小設定為16)。

let textSize:CGSize = myText.sizeWithAttributes([NSFontAttributeName: UIFont.systemFontOfSize(16)])
let textHeight = textSize.height + 10 //文字與線條間留點空隙
drawLine(y:textHeight,lineWidth: 1)

如要換新一頁,記得都要呼叫UIGraphicsBeginPDFPageWithInfo函數。確定完成所有PDF的內容繪製後,使用UIGraphicsEndPDFContext函數結束。

UIGraphicsEndPDFContext()

建立完成的檔案可從該APP的Documents資料夾找到。


該APP資料夾的路徑可透過下列方式查到:

  1. 設定Breakpoint
  2. 使用Xcode的除錯器LLDB。當執行到該行Console底下會出現(lldb),打入指令po NSHomeDirectory()即會出現路徑。


讀取PDF檔

1. 建立UIWebView

透過Storyboard、XIB或程式撰寫建立一個UIWebView,並由這個UIWebView顯示PDF檔案內容。我將這個UIWebView命名為showWebView。

2. 取得檔案的URL後,使用loadRequest載入

func showfile(){
     let fileName:String = "mypdf.pdf"
     let pathArr:NSArray = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
     let path = pathArr[0]
     let fileNamePath = path.stringByAppendingPathComponent(fileName)
     let url:NSURL = NSURL.fileURLWithPath(fileNamePath)
     let request:NSURLRequest = NSURLRequest(URL: url)
     showWebView.loadRequest(request)
}

印出、郵寄PDF檔

1. 使用Quick Look Framework

除了用UIWebView顯示PDF檔案內容外。如果有預覽、列印或轉寄PDF的需求,這時可採用Quick Look。使用此Framework須實作QLPreviewControllerDataSource協定內被委託的函數。裡面的函數分別是numberOfPreviewItemsInPreviewControllerpreviewController

記得一開始須連結Framework。

import QuickLook

接下來實作第一個numberOfPreviewItemsInPreviewController函數,此函數需回傳資料的筆數(也就是檔案的筆數)。contentsOfDirectoryAtPath函數可以回傳特定資料夾內的所有項目路徑,之後使用filteredArrayUsingPredicate函數搭配NSPredicate使用,過濾副檔名為pdf的項目,最後再回傳該陣列的元素數量即可。

關於NSPredicate,如需用到正規表達式去驗證資料時可拿來應用。在下面的程式例子中,檔名須符合"self ENDSWITH '.pdf'"這個格式。self代表字串本身,ENDSWITH代表該字串是否以指定的字串'.pdf'結尾。詳細的語法可見官網(Predicate Format String Syntax)。

var fileList:NSArray = []//用來儲存Documents資料夾內的所有檔名
let pathArr:NSArray = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    
func numberOfPreviewItemsInPreviewController(controller: QLPreviewController) -> Int {
     let path = pathArr[0]
     do{
         fileList = try NSFileManager.defaultManager().contentsOfDirectoryAtPath(path as! String)
         let filter:NSPredicate = NSPredicate(format: "self ENDSWITH '.pdf'")
         fileList = fileList.filteredArrayUsingPredicate(filter)
     }catch let error as NSError {
         print(error.debugDescription)
     }
     return fileList.count
}

最後實作previewController。當需預覽該索引(previewItemAtIndex)項目時即會調用此函數,回傳該項目的NSURL(檔案的所在路徑)。

func previewController(controller: QLPreviewController, previewItemAtIndex index: Int) -> QLPreviewItem {
     let fileName:String = fileList[index] as! String
     let path = pathArr[0]
     let fileNamePath = path.stringByAppendingPathComponent(fileName)
     let url:NSURL = NSURL.fileURLWithPath(fileNamePath)
     return url
}

完成後以Quick Look預覽PDF的畫面長這樣。



功能的實作大致上就是這樣了。不過有個問題也需記錄一下,在郵寄PDF的這個功能上,使用模擬器操作都會跳出這個錯誤(MailCompositionService quit unexpectedly)。拜了Google大神也不少人遇到這個問題,後來發現這個錯誤只會發生在模擬器,如果用實機操作就不會有這個問題。




以上。呼!終於打完了啊(T_T;)
目前放code的工具是使用SyntaxHighlighter,挺不錯的工具。我使用的是第2版。

參考資源:

沒有留言:

張貼留言