将 SwiftUI 视图渲染为 UIImage

Posted

技术标签:

【中文标题】将 SwiftUI 视图渲染为 UIImage【英文标题】:Render SwiftUI View as an UIImage 【发布时间】:2019-12-02 02:39:16 【问题描述】:

我正在尝试将 SwiftUI 视图呈现为 UIImage,然后让用户选择保存到相机胶卷或通过电子邮件发送给其他人。

例如,我想将 50 行的列表渲染到 UIImage 中。

struct MyList: View 
    var body: some View 
        List 
            ForEach(0 ..< 50, id:\.self) 
                Text("row \($0)")
            
        
    

过去几周在互联网上搜索仍然没有运气。我尝试了 2 种不同的方法。

1. UIHostingController (source here)

let hosting = UIHostingController(rootView: Text("TEST"))
hosting.view.frame = // Calculate the content size here //
let snapshot = hosting.view.snapshot        // output: an empty snapshot of the size
print(hosting.view.subviews.count)          // output: 0

我尝试了layoutSubviews()setNeedsLayout()layoutIfNeeded()loadView(),但仍然得到 0 个子视图。

2。 UIWindow.rootViewController (source here)

var vc = UIApplication.shared.windows[0].rootViewController
vc = vc.visibleController          // loop through the view controller stack to find the top most view controller
let snapshot = vc.view.snapshot    // output: a snapshot of the top most view controller

这几乎产生了我想要的输出。但是,我得到的快照实际上是一个屏幕截图,即带有导航栏、标签栏和固定大小(与屏幕大小相同)。我需要捕获的只是没有这些条的视图内容,有时可能比屏幕大(在这个例子中,我的视图是一个很长的列表)。

尝试查询vc.view.subviews 以查找我想要的表视图,但返回无用的[&lt;_TtGC7SwiftUI16PlatformViewHostGVS_42PlatformViewControllerRepresentableAdaptorGVS_16BridgedSplitViewVVS_22_VariadicView_Children7ElementGVS_5GroupGVS_19_ConditionalContentS4_GVS_17_UnaryViewAdaptorVS_9EmptyView______: 0x7fb061cc4770; frame = (0 0; 414 842); anchorPoint = (0, 0); tintColor = UIExtendedSRGBColorSpace 0 0.478431 1 1; layer = &lt;CALayer: 0x600002d8caa0&gt;&gt;]

非常感谢任何帮助。

【问题讨论】:

你似乎专注于 50 张图片。您是指单个屏幕上的 50 张图像吗?这很可能是不可能的。这样看 - List is 等同于 UITableView。什么样的设备一次显示50个cell/ @dfd 不是 50 张图片,而是 50 行。 50只是一个例子。让我这样说:我想创建一个包含所有这 50 行的 jpeg,以便可以将其保存到相机胶卷或通过电子邮件发送给其他人。 【参考方案1】:

这样的东西对你有用吗?

import SwiftUI

extension UIView 
    func takeScreenshot() -> UIImage 
        // Begin context
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale)
        // Draw view in that context
        drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        // And finally, get image
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        if (image != nil) 
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil);
            return image!
        

        return UIImage()
    


struct ContentView: UIViewRepresentable 
    func makeUIView(context: Context) -> UIView 
        let someView = UIView(frame: UIScreen.main.bounds)
        _ = someView.takeScreenshot()
        return someView
    

    func updateUIView(_ view: UIView, context: Context) 

    


struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

【讨论】:

感谢您的快速回复。等我回家后试试。会及时通知您。 @Anthony 如果您想为原始问题提供更多功能,我会问另一个问题。如果这个答案有助于考虑接受它。 恕我直言,您的答案是生成一个尺寸与屏幕尺寸相同的空白白色图像。虽然我一直在寻求一种方法来生成内容超过屏幕尺寸的视图图像。这就是我一直在问的问题,即使在我添加示例之前的问题的原始版本中也是如此。 无论如何,感谢您的帮助。我会将此答案标记为正确,并更清楚地提出另一个问题。再次感谢你。 :) 这个答案没用——他在问如何将 SwiftUI 视图渲染为图像,这个答案显示了如何将 UIView 渲染为图像。此答案不应标记为已接受。【参考方案2】:

我想知道为什么人们说你的问题不清楚。这实际上非常有意义,这是我一直希望自己做的事情之一。

我终于找到了一种按照你希望的方式做事的方法,虽然很复杂,但它确实有效。

这个想法是滥用你的第二种可能性(UIWindow.rootViewController)。那个很有趣,因为您打开窗口,确定要绘制的内容,然后绘制它。

但问题是这是您的主窗口,而您要求绘制主窗口。

我最终得到它的方法是执行UIViewControllerRepresentable,它在我的 SwiftUI 应用程序中给了我一个UIViewController。然后,在其中,我放了一个 UIHostingController,它在 UIView 中为我提供了一个 SwiftUI 视图。换句话说,我做了一座桥。

update 函数中,我可以调用异步调度到view.layer.render(context) 来绘制图像。

有趣的是,UIViewControllerRepresentable 实际上会全屏显示。所以我建议做一个.scale(...),这样你的整个列表就可以放在屏幕上(渲染命令不会介意它变小)并且它会居中。因此,如果您事先知道图像的大小,则可以使用HStack VStack YourStuff Spacer() Spacer() 使其呈现在左上角。

祝你好运!

【讨论】:

以上是关于将 SwiftUI 视图渲染为 UIImage的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 中自定义网格视图的额外不必要渲染问题

SwiftUI 绑定到父视图重新渲染子视图

SwiftUI 布局(Layout)基础

如何在 SwiftUI 中实现 MVVM 模式?视图不会重新渲染

SwiftUI:避免使用 MKMapView+UIViewRepresentable 在 TabView 中重新创建/重新渲染视图

如何使用 SwiftUI 连续渲染软件位图?