简体   繁体   English

在 iOS 上将 SwiftUI 视图转换为 PDF

[英]Convert SwiftUI View to PDF on iOS

I was drawing some nice graphs with SwiftUI, because it is so simple and easy to do.我正在用 SwiftUI 绘制一些漂亮的图形,因为它非常简单易行。 Then I wanted to export the whole SwiftUI View to a PDF such that someone else can view the graphs in a nice way.然后我想将整个 SwiftUI 视图导出为 PDF,以便其他人可以以一种很好的方式查看图形。 SwiftUI does not offer a solution for this directly. SwiftUI 没有直接为此提供解决方案。

Cheers,干杯,
Alex亚历克斯

After some thinking I came up with the idea of combining the UIKit to PDF method and SwiftUI.经过一番思考,我想到了将 UIKit 转 PDF 方法和 SwiftUI 结合起来的想法。

At first you create your SwiftUI view, then you put into an UIHostingController.首先创建 SwiftUI 视图,然后放入 UIHostingController。 You render the HostingController on a window behind all other views and and draw its layer on a PDF.您在所有其他视图后面的窗口上渲染 HostingController 并在 PDF 上绘制其图层。 Sample code is listed below.下面列出了示例代码。

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()
}

I loved this answer, but couldn't get it to work.我喜欢这个答案,但无法让它发挥作用。 I was getting an exception and the catch wasn't being executed.我遇到了一个异常并且没有执行捕获。

After some head scratching, and writing up a SO Question asking how to debug it (which I never submitted), I realized that the solution, while not obvious, was simple: wrap the rendering phase in an async call to the main queue:经过一番摸索,并写了一个 SO Question 询问如何调试它(我从未提交过),我意识到解决方案虽然不明显,但很简单:将渲染阶段包装在对主队列的异步调用中:

//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)")
     }
 }

Thanks, SnOwfreeze!谢谢,SnOwfreeze!

I tried all the answers, but they didn't work for me (Xcode 12.4, iOS 14.4).我尝试了所有答案,但它们对我不起作用(Xcode 12.4、iOS 14.4)。 Here is what's worked for me:这是对我有用的:

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()
    }
}

Note: Don't try to use this function in a view with .onAppear when you try to export the same view.注意:当您尝试导出相同的视图时,不要尝试在带有 .onAppear 的视图中使用此功能。 This will result in a endless loop naturally.这自然会导致无限循环。

When I tried to generate a PDF file using the the solutions in other answers I only got a blurred PDF and the quality was far from good.当我尝试使用其他答案中的解决方案生成 PDF 文件时,我只得到了一个模糊的 PDF 并且质量很差。

I ended up generating the SwiftUI view in a much bigger frame and scaled down the context to the appropriate size.我最终在一个更大的框架中生成了 SwiftUI 视图,并将上下文缩小到适当的大小。

Here are my modifications to the Sn0wfreeze's answer :这是我对Sn0wfreeze 的回答的修改:

// 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)")
}
...

I also used the following extension to multiply CGSize by CGFloat:我还使用以下扩展将 CGSize 乘以 CGFloat:

extension CGSize {
    static func * (size: CGSize, value: CGFloat) -> CGSize {
        return CGSize(width: size.width * value, height: size.height * value)
    }
}

I tried the other methods above but still had issues with the view showing properly, so here is what I have that works for me on Xcode 12.2 & Swift 5 after messing with both Sn0wfreeze's Answer & pawello2222's Answer :我尝试了上面的其他方法,但仍然无法正确显示视图,所以这是我在 Xcode 12.2 和 Swift 5 上对我有用的方法,在弄乱了Sn0wfreeze 的答案pawello2222 的答案之后

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)")
        }
    }
}

Basically, I removed any reference to a newly created "pdfVC" view controller, and only referenced the main view the entire time as our "rootVC".基本上,我删除了对新创建的“pdfVC”视图控制器的任何引用,并且始终将主视图引用为我们的“rootVC”。 I grab the screen width and height and use that to define our PDF's width and height, rather than trying to scale the view to proper A4 or letter size.我抓取屏幕宽度和高度并使用它来定义我们的 PDF 的宽度和高度,而不是尝试将视图缩放到适当的 A4 或字母大小。 This creates a PDF of the entire showing SwiftUI view.这将创建整个显示 SwiftUI 视图的 PDF。

Hope this helps anyone that had any issues with the other answers!希望这可以帮助任何对其他答案有任何问题的人! :) :)

func exportToPDF() {
    
    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: self)// when we render the current view
    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))
    DispatchQueue.main.async {
        do {
            try pdfRenderer.writePDF(to: outputFileURL) { context in
                context.beginPage()
                context.cgContext.scaleBy(x: 1 / dpiScale, y: 1 / dpiScale)
                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()
    }
}

when i tried to render the current view i got a warning.当我尝试渲染当前视图时,我收到了警告。 I fixed it.我修好了它。 And I added a line so that the pdf document remains light when dark mode is turned on in the device.我添加了一行,以便在设备中打开暗模式时 pdf 文档保持亮起。 It's the best solution for me, I hope for you too.这对我来说是最好的解决方案,我也希望你。 Thanks pawello2222谢谢pawello2222

here is the required extension from pawello2222这是 pawello2222 所需的扩展名

extension CGSize {
static func * (size: CGSize, value: CGFloat) -> CGSize {
    return CGSize(width: size.width * value, height: size.height * value)
   }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM