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 17 App 程式設計實戰心法》、《iOS 17 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
更多來自 AppCoda 中文版
iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場
SwiftUI 框架

iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場

Apple 的工程師可能早已認識到,許多 iOS 開發者都希望能夠重現 App Store 應用程式中的優雅 Hero 動畫。由於從頭實現這種動畫通常需要耗費大量時間與精力,Apple 在 iOS 18 SDK 中納入了這項功能。 透過這次更新,你現在只需少量的程式碼就能在自己的應用程式中實現類似的動畫過渡效果。這項重大改進讓開發者能夠創造出更具視覺吸引力且流暢的過渡效果,
如何使用 Vision APIs 從圖像中辨識文字
AI

如何使用 Vision APIs 從圖像中辨識文字

Vision 框架長期以來一直包含文字識別功能。我們已經有詳細的教程,向你展示如何使用 Vision 框架掃描圖像並執行文字識別。之前,我們使用了 VNImageRequestHandler 和 VNRecognizeTextRequest 來從圖像中提取文字。 多年來,Vision 框架已經顯著演變。在 iOS 18 中,Vision
iOS 18更新:SwiftUI 新功能介紹
SwiftUI 框架

iOS 18更新:SwiftUI 新功能介紹

SwiftUI的技術不斷演進,每次更新都讓 iOS 應用程式開發變得更加便捷。隨著 iOS 18 Beta 的推出,SwiftUI 引入了多個令人興奮的新功能,使開發者僅需幾行程式碼即可實現出色的效果。 本教學文章旨在探索這個版本中的幾項主要改進,幫助你了解如何運用這些新功能。 浮動標籤列 (Floating Tab Bar)SwiftUI中的標籤視圖(Tab
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。