将SwiftUI视图转换为iOS上的PDF

20

我使用SwiftUI绘制了一些漂亮的图形,因为它非常简单易用。然后我希望将整个SwiftUI视图导出为PDF,这样别人就可以以一种漂亮的方式查看图形。

SwiftUI并没有直接提供解决方案。

祝好,
Alex


有人能够从ScrowView或可滚动内容中获取多个pdf页面吗? - Heyman
6个回答

20

经过一番思考,我想到了将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()
}

有视图但没有文本。 - Sylar
不幸的是,这个解决方案提供的PDF带有光栅化文本。这意味着:1. 无法从PDF中选择文本。2. 文本看起来不像基于矢量的文本那样清晰。 - Eugene P

10

我喜欢这个答案,但是无法使其工作。我一直在收到异常并且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!


1
这是一个不错的采纳。我认为这可能会因 SwiftUI / Xcode 版本的不同而有所不同。当我更新到较新的 Xcode 版本时,我也遇到了类似的问题。 - Sn0wfreeze
我在Swift Playground中尝试了这个,但仍然出现了“表达式无法解析,未知错误”的提示,请问你有什么想法吗?你使用的是哪个Xcode版本?谢谢。 - Max0u
你记得要导入 "PDFKit" 吗? - Mozahler
是的,我只需要将 pdfVC.view.layer.render(in: context.cgContext) 替换为 rootVC.view.layer.render(in: context.cgContext) 就可以让它正常工作了。 - Max0u

4

我尝试了所有的答案,但它们对我都不起作用(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()
    }
}

注意:不要试图在使用.onAppear的视图中尝试使用此函数,当您尝试导出相同的视图时,这将导致自然的无限循环。

1
当我尝试使用其他答案中的解决方案生成PDF文件时,我只得到了一个模糊的PDF,质量远不如意。
最终我将SwiftUI视图生成在一个更大的框架中,并将上下文缩小到适当的大小。
以下是我的修改Sn0wfreeze's answer
// 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)")
}
...

我还使用了以下扩展将CGSize乘以CGFloat:
extension CGSize {
    static func * (size: CGSize, value: CGFloat) -> CGSize {
        return CGSize(width: size.width * value, height: size.height * value)
    }
}

当我实现这个功能时,我的视图比PDF框架要小得多,有什么想法吗? - benpomeroy9
@benpomeroy9 你也扩大了视图框架吗?这个想法是将视图放大,然后使用 UIGraphicsPDFRenderer 缩小它,而不改变 PDF 页面大小。 - pawello2222

0
 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))])}
  }
}

你如何将具有可滚动内容的视图(例如ContentView())转换为此解决方案?如果视图没有任何行,你将如何确定“rowCounts”? - Heyman
函数必须位于ContentView()中,并且可能是:let rootView = self - Evgeny Lisin

0

我尝试了上面的其他方法,但仍然存在视图显示问题,所以这是我在 Xcode 12.2 和 Swift 5 上使用的有效方法,经过对 Sn0wfreeze's Answerpawello2222'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)")
        }
    }
}

基本上,我删除了对新创建的“pdfVC”视图控制器的任何引用,并且在整个时间内仅引用主视图作为我们的“rootVC”。我获取屏幕宽度和高度,并使用它来定义我们的PDF的宽度和高度,而不是尝试将视图缩放到适当的A4或信纸大小。这将创建显示SwiftUI视图的整个PDF。
希望这可以帮助任何遇到其他问题的人! :)

文件在哪里?我有一个路径,但是我无法进入查看我的文件... 非常感谢! - Antonio Labra
你可以通过从设备下载容器来获取它。它位于文档文件夹中。但这种解决方案的问题在于它无法调整到页面大小,特别是那些与屏幕不同纵横比的页面 - 这是当前所有设备屏幕尺寸的情况。此外,渲染效果有点不对。 - ngb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接