我使用SwiftUI绘制了一些漂亮的图形,因为它非常简单易用。然后我希望将整个SwiftUI视图导出为PDF,这样别人就可以以一种漂亮的方式查看图形。
SwiftUI并没有直接提供解决方案。
祝好,
Alex
经过一番思考,我想到了将UIKit和SwiftUI方法结合的主意。
首先,您需要创建自己的SwiftUI视图,然后将其放入一个UIHostingController中。您在窗口上呈现HostingController,在所有其他视图之后绘制其层,并将其绘制在PDF上。下面是示例代码。
func exportToPDF() {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let outputFileURL = documentDirectory.appendingPathComponent("SwiftUI.pdf")
//Normal with
let width: CGFloat = 8.5 * 72.0
//Estimate the height of your view
let height: CGFloat = 1000
let charts = ChartsView()
let pdfVC = UIHostingController(rootView: charts)
pdfVC.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
//Render the view behind all other views
let rootVC = UIApplication.shared.windows.first?.rootViewController
rootVC?.addChild(pdfVC)
rootVC?.view.insertSubview(pdfVC.view, at: 0)
//Render the PDF
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 8.5 * 72.0, height: height))
do {
try pdfRenderer.writePDF(to: outputFileURL, withActions: { (context) in
context.beginPage()
pdfVC.view.layer.render(in: context.cgContext)
})
self.exportURL = outputFileURL
self.showExportSheet = true
}catch {
self.showError = true
print("Could not create PDF file: \(error)")
}
pdfVC.removeFromParent()
pdfVC.view.removeFromSuperview()
}
我喜欢这个答案,但是无法使其工作。我一直在收到异常并且catch块没有被执行。
经过一些思考,并编写了一个 Stack Overflow 问题来询问如何调试它(我从未提交),我意识到解决方案虽然不明显,但很简单:将渲染阶段包装在主队列的异步调用中:
//Render the PDF
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 8.5 * 72.0, height: height))
DispatchQueue.main.async {
do {
try pdfRenderer.writePDF(to: outputFileURL, withActions: { (context) in
context.beginPage()
pdfVC.view.layer.render(in: context.cgContext)
})
print("wrote file to: \(outputFileURL.path)")
} catch {
print("Could not create PDF file: \(error.localizedDescription)")
}
}
谢谢,SnOwfreeze!
pdfVC.view.layer.render(in: context.cgContext)
替换为 rootVC.view.layer.render(in: context.cgContext)
就可以让它正常工作了。 - Max0u我尝试了所有的答案,但它们对我都不起作用(Xcode 12.4,iOS 14.4)。以下是对我有效的方法:
import SwiftUI
func exportToPDF() {
let outputFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("SwiftUI.pdf")
let pageSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
//View to render on PDF
let myUIHostingController = UIHostingController(rootView: ContentView())
myUIHostingController.view.frame = CGRect(origin: .zero, size: pageSize)
//Render the view behind all other views
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
print("ERROR: Could not find root ViewController.")
return
}
rootVC.addChild(myUIHostingController)
//at: 0 -> draws behind all other views
//at: UIApplication.shared.windows.count -> draw in front
rootVC.view.insertSubview(myUIHostingController.view, at: 0)
//Render the PDF
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: pageSize))
DispatchQueue.main.async {
do {
try pdfRenderer.writePDF(to: outputFileURL, withActions: { (context) in
context.beginPage()
myUIHostingController.view.layer.render(in: context.cgContext)
})
print("wrote file to: \(outputFileURL.path)")
} catch {
print("Could not create PDF file: \(error.localizedDescription)")
}
//Remove rendered view
myUIHostingController.removeFromParent()
myUIHostingController.view.removeFromSuperview()
}
}
// scale 1 -> 72 DPI
// scale 4 -> 288 DPI
// scale 300 / 72 -> 300 DPI
let dpiScale: CGFloat = 4
// for US letter page size
let pageSize = CGSize(width: 8.5 * 72, height: 11 * 72)
// for A4 page size
// let pageSize = CGSize(width: 8.27 * 72, height: 11.69 * 72)
let pdfVC = UIHostingController(rootView: swiftUIview)
pdfVC.view.frame = CGRect(origin: .zero, size: pageSize * dpiScale)
...
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: pageSize))
do {
try pdfRenderer.writePDF(to: outputFileURL) { context in
context.beginPage()
context.cgContext.scaleBy(x: 1 / dpiScale, y: 1 / dpiScale)
pdfVC.view.layer.render(in: context.cgContext)
}
print("File saved to: \(outputFileURL.path)")
}
...
extension CGSize {
static func * (size: CGSize, value: CGFloat) -> CGSize {
return CGSize(width: size.width * value, height: size.height * value)
}
}
UIGraphicsPDFRenderer
缩小它,而不改变 PDF 页面大小。 - pawello2222 func exportToPDF() {
let listHeight: CGFloat = 24.0//or you can get it from your ListView
let rowsCount = yourArrayOrObject.count
let rowsCountPerPage = Int(11.69 * 72 / listHeight) - 6
let countOfPages = Int(ceil( Double(rowsCount) / Double(rowsCountPerPage)))
var index = 0
while index < countOfPages {
let items = yourArrayOrObject.slice(size: rowsCountPerPage)[index]
let rootView = List {
ForEach(items, id: \.self) { item in
Text("\(item.name)")
}
}
let dpiScale: CGFloat = 1.5
// for US letter page size
// let pageSize = CGSize(width: 8.5 * 72, height: 11 * 72)
// for A4 page size
let pageSize = CGSize(width: 8.27 * 72, height: 11.69 * 72)
//View to render on PDF
let myUIHostingController = UIHostingController(rootView: rootView)
myUIHostingController.view.frame = CGRect(origin: .zero, size: pageSize * dpiScale)
myUIHostingController.view.overrideUserInterfaceStyle = .light//it will be light, when dark mode
//Render the view behind all other views
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else {
print("ERROR: Could not find root ViewController.")
return
}
rootVC.addChild(myUIHostingController)
//at: 0 -> draws behind all other views
//at: UIApplication.shared.windows.count -> draw in front
rootVC.view.insertSubview(myUIHostingController.view, at: 0)
//Render the PDF
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: pageSize))
let data = pdfRenderer.pdfData { context in
context.beginPage()
context.cgContext.scaleBy(x: 1 / dpiScale, y: 1 / dpiScale)
myUIHostingController.view.layer.render(in: context.cgContext)
}
//Remove rendered view
myUIHostingController.removeFromParent()
myUIHostingController.view.removeFromSuperview()
if index == 0 {
pdfDocumentData = data // pdfDocumentData must be declared above: @State private var pdfDocumentData = Data()(don't forget import PDFKit)
} else {
pdfDocumentData = mergePdf(data: pdfDocumentData, otherPdfDocumentData: data).dataRepresentation()!
}
index += 1
}
writeDataToTemporaryDirectory(withFilename: "YourPDFFileName.pdf", data: pdfDocumentData)
}
private func writeDataToTemporaryDirectory(withFilename: String, data: Data) {
do {
// name the file
let temporaryFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(withFilename)
print("writeDataToTemporaryDirectory at url:\t\(temporaryFileURL)")
try data.write(to: temporaryFileURL, options: .atomic)
print("It's Ok!")//you can open file in sheet: (self.showReport = true) ,for example
pdfDocumentData = Data()
} catch {
print(error)
}
}
func mergePdf(data: Data, otherPdfDocumentData: Data) -> PDFDocument {
// get the pdfData
let pdfDocument = PDFDocument(data: data)!
let otherPdfDocument = PDFDocument(data: otherPdfDocumentData)!
// create new PDFDocument
let newPdfDocument = PDFDocument()
// insert all pages of first document
for p in 0..<pdfDocument.pageCount {
let page = pdfDocument.page(at: p)!
newPdfDocument.insert(page, at: newPdfDocument.pageCount)
}
// insert all pages of other document
for q in 0..<otherPdfDocument.pageCount {
let page = otherPdfDocument.page(at: q)!
newPdfDocument.insert(page, at: newPdfDocument.pageCount)
}
return newPdfDocument
}
适用于 XCode 13, iOS 15,这是一个多页面解决方案
pdfDocumentData 必须在上面声明:@State private var pdfDocumentData = Data() (不要忘记导入 PDFKit) 谢谢 pawello2222
以下是一些必需的扩展功能
extension CGSize {
static func * (size: CGSize, value: CGFloat) -> CGSize {
return CGSize(width: size.width * value, height: size.height * value)
}
}
extension Array {
func slice(size: Int) -> [[Element]] {
(0...(count / size)).map{Array(self[($0 * size)..<(Swift.min($0 * size + size, count))])}
}
}
我尝试了上面的其他方法,但仍然存在视图显示问题,所以这是我在 Xcode 12.2 和 Swift 5 上使用的有效方法,经过对 Sn0wfreeze's Answer 和 pawello2222's Answer 的调整:
func exportToPDF() {
let outputFileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("SwiftUI.pdf")
let pageSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
let rootVC = UIApplication.shared.windows.first?.rootViewController
//Render the PDF
let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: pageSize))
DispatchQueue.main.async {
do {
try pdfRenderer.writePDF(to: outputFileURL, withActions: { (context) in
context.beginPage()
rootVC?.view.layer.render(in: context.cgContext)
})
print("wrote file to: \(outputFileURL.path)")
} catch {
print("Could not create PDF file: \(error.localizedDescription)")
}
}
}