将SwiftUI视图渲染为UIImage

11
我试图将SwiftUI视图渲染为UIImage,然后让用户选择保存到相机胶卷还是通过电子邮件发送给其他人。
例如,我想将50行列表渲染成UIImage。
struct MyList: View {
    var body: some View {
        List {
            ForEach(0 ..< 50, id:\.self) {
                Text("row \($0)")
            }
        }
    }
}

过去几周在网络上搜索了很久,但仍然没有找到。 我尝试了两种不同的方法。

1. UIHostingController (这里是来源)

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(),但仍然没有子视图。 2. UIWindow.rootViewController (源代码在这里)
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以查找我想要的表格视图,但返回一个无用的[<_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 = <CALayer: 0x600002d8caa0>>]

非常感谢任何帮助。




你似乎固定在50张图片上。你是在谈论单个屏幕上的50张图片吗?那很可能不可能。从这个角度来看 - 一个List等同于一个UITableView。什么样的设备可以同时显示50个单元格? - user7014451
@dfd 不是50张图片,而是50行。50只是一个例子。让我这样说吧:我想创建一个包含所有这50行的JPEG图像,以便可以保存到相机胶卷或通过电子邮件发送给其他人。 - Anthony
2个回答

7
这样的方案对您有用吗?
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
1
@Anthony,如果您想要给原来的问题增加更多的功能,我可以为您提出另一个问题。如果我的回答对您有帮助,请考虑接受它。 - fulvio
2
请恕我直言,您的回答生成了一个大小与屏幕相同的空白白色图像。而我一直在寻找一种方法来生成一个视图图像,其中包含超出屏幕大小的内容。这就是我一直在寻找的,即使在我添加示例之前的问题原始版本中也是如此。 - Anthony
1
无论如何,感谢您的帮助。我会将这个答案标记为正确,并提出另一个更清晰的问题。再次感谢您。 :) - Anthony
4
这个回答没有用——他问的是如何将一个SwiftUI视图渲染成图像,而这个回答展示了如何将一个UIView渲染成图像。这个回答不应该被标记为被接受。 - sahandnayebaziz
显示剩余4条评论

3
我想知道为什么人们说你的问题不清楚。实际上它非常有道理,这也是我一直在寻找的事情之一。
我最终找到了一种方法来按照您所希望的方式完成任务,虽然它很复杂,但确实有效。
思路是滥用您的第二个可能性(`UIWindow.rootViewController`)。这个很有趣,因为你拿到窗口后,就可以确定你想绘制什么,然后就可以绘制它了。
但问题是这是你的主窗口,你要求绘制你的主窗口。
我最终让它工作的方法是使用 `UIViewControllerRepresentable`,它能给我一个在 SwiftUI 应用程序中的 `UIViewController`。然后,在里面放置一个单独的 `UIHostingController`,它能让我在 UIView 中得到 SwiftUI 视图。换句话说,我做了一个桥接。
在 `update` 函数中,我可以调用一个分派异步到 `view.layer.render(context)` 来绘制图像。
值得注意的是,`UIViewControllerRepresentable` 实际上将是全屏的。因此,我建议使用 `.scale(...)` 让整个列表适合屏幕(渲染命令不介意缩小),并且它将居中。所以,如果你事先知道你的图像大小,你可以做一个 `HStack { VStack { YourStuff Spacer() } Spacer() }`,这样就会在左上角呈现。
祝你好运!

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