【问题标题】:Convert SwiftUI View to PDF on iOS在 iOS 上将 SwiftUI 视图转换为 PDF
【发布时间】:2020-06-30 08:48:51
【问题描述】:

我正在使用 SwiftUI 绘制一些漂亮的图表,因为它非常简单易行。然后我想将整个 SwiftUI 视图导出为 PDF,这样其他人就可以很好地查看图表。 SwiftUI 没有直接为此提供解决方案。

干杯,
亚历克斯

【问题讨论】:

  • 有没有人能够从 ScrowView 中获取多个 pdf 页面或者只是一个可滚动的内容?

标签: ios pdf uikit swiftui


【解决方案1】:
 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
  • Function 必须在 ContentView() 中,可能:let rootView = self
【解决方案2】:

我尝试了所有答案,但它们对我不起作用(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 的视图中使用此功能。这自然会导致无限循环。

【讨论】:

    【解决方案3】:

    我尝试了上面的其他方法,但仍然无法正确显示视图,所以在搞砸了Sn0wfreeze's Answerpawello2222's Answer 之后,这就是我在 Xcode 12.2 和 Swift 5 上对我有用的方法:

    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。

    希望这可以帮助任何对其他答案有任何问题的人! :)

    【讨论】:

    • 文件在哪里?我有一个路径,但我无法进入查看我的文件...非常感谢!
    • 您可以通过从设备下载容器来获取它。它在文档文件夹中。但是这个解决方案的问题是它不会调整到与屏幕宽高比不同的页面大小 - 这是所有当前设备屏幕大小的情况。渲染也有点不对。
    【解决方案4】:

    当我尝试使用其他答案中的解决方案生成 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 你也放大了视图框吗?这个想法是放大视图,然后使用UIGraphicsPDFRenderer 将其缩小,而不更改 PDF 页面大小。
    【解决方案5】:

    我喜欢这个答案,但无法让它发挥作用。我遇到了一个异常,并且 catch 没有被执行。

    经过一番摸索,并写了一个 SO Question 询问如何调试它(我从未提交过),我意识到解决方案虽然不明显,但很简单:将渲染阶段包装在对 main 的异步调用中队列:

    //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!

    【讨论】:

    • 这是一个很好的采用。我认为这可能会因 SwiftUI / Xcode 版本而有所不同。当我更新到较新的 Xcode 版本时,我遇到了类似的问题
    • 我在 Swift Playground 中尝试了这个,但仍然得到“表达式解析失败,未知错误”你知道吗?您使用的是哪个 Xcode 版本?谢谢
    • 你记得“导入 PDFKit”吗?
    • 是的,我只需要将 pdfVC.view.layer.render(in: context.cgContext) 替换为 rootVC.view.layer.render(in: context.cgContext) 即可使用
    【解决方案6】:

    经过一番思考,我想出了将 UIKit 与 PDF 方法和 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()
    }
    

    【讨论】:

      猜你喜欢
      • 2021-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-26
      相关资源
      最近更新 更多