第 42 章
如何把 SwiftUI 視圖轉換為 PDF 文件
在上一章,我們學習了如何使用 ImageRenderer
擷取 SwiftUI 視圖,並儲存為圖像。這個在 iOS 16 推出的新類別還可以把視圖轉換為 PDF 文件。
在這篇教學中,我們會以上次的範例為基礎進行構建,並添加 Save to PDF 功能。
重溫上一篇文章的範例 App
如果你還沒有讀過上一章,我建議你先讀過再看此章節。在上一篇文章中,我們已經說明過 ImageRenderer
的基本用法,並解釋了範例 App 的實作過程。
要跟著實作的話,可以先下載這個 Starter 項目:https://www.appcoda.com/resources/swiftui4/SwiftUIImageRendererPDFStarter.zip.
我對上次的範例 App 做了一些改動,為折線圖添加了 heading 和 caption。範例 App 現在還有一個 PDF 按鈕,用來把 Chart 視圖保存為 PDF 文件。以下是 ChartView
結構的程式碼:
struct ChartView: View {
let chartData = [ (city: "Hong Kong", data: hkWeatherData),
(city: "London", data: londonWeatherData),
(city: "Taipei", data: taipeiWeatherData)
]
var body: some View {
VStack {
Text("Building Line Charts in SwiftUI")
.font(.system(size: 40, weight: .heavy, design: .rounded))
.multilineTextAlignment(.center)
.padding()
Chart {
ForEach(chartData, id: \.city) { series in
ForEach(series.data) { item in
LineMark(
x: .value("Month", item.date),
y: .value("Temp", item.temperature)
)
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.month(.defaultDigits))
}
}
.chartPlotStyle { plotArea in
plotArea
.background(.blue.opacity(0.1))
}
.chartYAxis {
AxisMarks(position: .leading)
}
.frame(width: 350, height: 300)
.padding(.horizontal)
Text("Figure 1. Line Chart")
.padding()
}
}
}
把 Chart 視圖儲存為 PDF 文件
現在,我們想要做的就是使用 ImageRenderer
,把 ChartView
儲存為 PDF 文件。雖然,我們只需要幾行程式碼就可以把 SwiftUI 視圖轉換為圖像,PDF rendering 就需要多一點步驟。
如果我們是想把 Chart 轉換為圖像,我們可以存取 uiImage
屬性,來取得渲染圖像 ( rendered image)。而如果我們想把 Chart 轉換成 PDF,就會使用 ImageRenderer
的 render
方法。我們將會實作:
- 尋找文檔目錄,並為 PDF 文件準備渲染路徑 (rendered path)(例如 linechart.pdf)。
- 準備一個用於繪圖的
CGContext
實例。 - 調用渲染器 (renderer) 的
render
方法來渲染 PDF 文件。
在實作中,我們會建立一個新方法 exportPDF
。以下是這個方法的程式碼:
@MainActor
private func exportPDF() {
guard let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let renderedUrl = documentDirectory.appending(path: "linechart.pdf")
if let consumer = CGDataConsumer(url: renderedUrl as CFURL),
let pdfContext = CGContext(consumer: consumer, mediaBox: nil, nil) {
let renderer = ImageRenderer(content: chartView)
renderer.render { size, renderer in
let options: [CFString: Any] = [
kCGPDFContextMediaBox: CGRect(origin: .zero, size: size)
]
pdfContext.beginPDFPage(options as CFDictionary)
renderer(pdfContext)
pdfContext.endPDFPage()
pdfContext.closePDF()
}
}
print("Saving PDF to \(renderedUrl.path())")
}
在程式碼的前兩行,我們檢索了使用者的文檔目錄,並設置 PDF 文件的文件路徑(即 line chart.pdf
)。然後,我們創建了 CGContext
的實例,並把 mediaBox
參數設置為 nil。在這種情況下,Core Graphics 會使用預設頁面大小 8.5 x 11 inches(612 x 792 points)。
renderer
閉包會接收兩個參數:視圖當前的大小、和將視圖渲染到 CGContext
的函數。讓我們調用上下文的 beginPDFPage
方法,來打開 PDF 頁面。renderer
方法會繪製 Chart 視圖。請記住,我們需要關閉 PDF 文件才能完成整個操作。
讓我們要創建一個 PDF
按鈕,來調用這個 exportPDF
方法:
Button {
exportPDF()
} label: {
Label("PDF", systemImage: "doc.plaintext")
}
.buttonStyle(.borderedProminent)
我們可以試著在模擬器上執行 App 來進行測試,點擊 PDF 按鈕後,你應該會在控制台看到以下訊息:
Saving PDF to /Users/simon/Library/Developer/CoreSimulator/Devices/CA9B849B-36C5-4608-9D72-B04C468DA87E/data/Containers/Data/Application/04415B8A-7485-48F0-8DA2-59B97C2B529D/Documents/linechart.pdf
如果你在 Finder 打開文件,應該會看到以下的 PDF 文件:
如果想調整圖表的位置,我們可以在調用 renderer
前插入這行程式碼:
pdfContext.translateBy(x: 0, y: 200)
如此一來,圖表就會出現在文件上面的部分。
將 PDF 檔加至 Files app
你可能會發現我們無法在 Files App 中找到 PDF 文件。如果我們想 PDF 文件可用於內置的 Files App,我們就需要在 Info.plist
中更改一些設置。讓我們切換到 Info.plist
並添加以下的 key:
UIFileSharingEnabled
- 讓 App 支援 iTunes 文件分享LSSupportsOpeningDocumentsInPlace
- 支援在本地打開文件
然後,把 key 的值設置為 Yes。啟用了這兩個選項後,讓我們再在模擬器上執行 App。接著,打開 Files App 並導航到 On My iPhone 位置,你應該會看到 App 的資料夾,而 PDF 文件就在資料夾中。
總結
你不僅可以從視圖創建圖像,ImageRenderer
還允許開發人員將視圖轉換為 PDF 文檔。 借助 iOS 16 中的這個新 API,你可以輕鬆地在 iOS 應用程序中添加一些 PDF 相關的功能。
想更深入學習SwiftUI和下載完整程式碼?你可以從 AppCoda網站購買《精通 SwiftUI》完整電子版。