SwiftUI 框架

SwiftUI ImageRenderer:如何把 SwiftUI 視圖轉換為 PDF 文件

在上一篇文章中,我們學習了如何使用 ImageRenderer 擷取 SwiftUI 視圖,並儲存為圖像。這個在 iOS 16 推出的新類別還可以把視圖轉換為 PDF 文件。在這篇文章中,我會以上次的範例為基礎進行構建,並添加 Save to PDF 功能。
SwiftUI ImageRenderer:如何把 SwiftUI 視圖轉換為 PDF 文件
Photo by Thai Nguyen on Unsplash
SwiftUI ImageRenderer:如何把 SwiftUI 視圖轉換為 PDF 文件
Photo by Thai Nguyen on Unsplash
In: SwiftUI 框架

在上一篇文章中,我們學習了如何使用 ImageRenderer 擷取 SwiftUI 視圖,並儲存為圖像。這個在 iOS 16 推出的新類別還可以把視圖轉換為 PDF 文件。

在這篇教學中,我們會以上次的範例為基礎進行構建,並添加 Save to PDF 功能。如果要跟著這篇文章進行實作,大家需要使用 Xcode 14 beta 3 或以上的版本。

重溫上一篇文章的範例 App

如果你還沒有讀過上一篇教學文章,我建議你先讀過再看這篇文章。在上一篇文章中,我們已經說明過 ImageRenderer 的基本用法,並解釋了範例 App 的實作過程。

swiftui-imagerenderer-pdf-demo

我對上次的範例 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()

        }
    }
}

利用 ImageRenderer 把 Chart 視圖儲存為 PDF 文件

現在,我們想要做的就是使用 ImageRenderer,把 ChartView 儲存為 PDF 文件。雖然,我們只需要幾行程式碼就可以把 SwiftUI 視圖轉換為圖像,PDF rendering 就需要多一點步驟。

如果我們是想把 Chart 轉換為圖像,我們可以存取 uiImage 屬性,來取得渲染圖像 ( rendered image)。而如果我們想把 Chart 轉換成 PDF,就會使用 ImageRendererrender 方法。我們將會實作:

  • 尋找文檔目錄,並為 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 文件:

swiftui-line-chart-pdf

如果想調整圖表的位置,我們可以在調用 renderer 前插入這行程式碼:

pdfContext.translateBy(x: 0, y: 200)

如此一來,圖表就會出現在文件上面的部分。

swiftui-line-chart-adjusted

Make the PDF file available to the 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 文件就在資料夾中。

swiftui-files-app-pdf

如你有興趣深入學習 SwiftUI,歡迎查閱我們的《精通 SwiftUI》一書。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。

作者
Simon Ng
軟體工程師,AppCoda 創辦人。著有《iOS 18 App 程式設計實戰心法》、《iOS 18 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
更多來自 AppCoda 中文版
如何在 SwiftUI App 中開發 Live Activities
SwiftUI 框架

如何在 SwiftUI App 中開發 Live Activities

Live Activities 首次於 iOS 16 推出,是 Apple 最令人興奮的更新之一,能讓 App 與使用者在即時互動上更有連結。它不再需要使用者不斷打開 App,Live Activities 可以讓資訊直接顯示在鎖定畫面和 Dynamic Island 上。
使用 Tool Calling 強化 Foundation Models 功能
AI

使用 Tool Calling 強化 Foundation Models 功能

在前幾篇教學中,我們介紹了 Foundation Models 在 iOS 26 中的運作方式,以及如何使用這個全新框架打造具備 AI 功能的應用。我們也介紹了 @Generable 巨集,它能輕鬆地將模型回應轉換為結構化的 Swift 類型。 現在,在這個 Foundation
活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App
AI

活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App

在前一篇教學中,我們介紹了 Foundation Models 框架,並示範了如何用它來進行基本的內容生成。那個過程相當簡單——你提供一個提示詞(prompt),等幾秒鐘,就能獲得自然語言的回應。在我們的範例中,我們建立了一個簡單的問答 App,讓使用者可以提問,App 則直接顯示生成的文字。 但如果回應變得更複雜——你需要把非結構化文字轉換為結構化的物件呢? 舉例來說,
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。