如何将不在根层次结构中的SwiftUI视图渲染为UIImage?

12

假设我有一个简单的SwiftUI视图,不是ContentView,例如:

struct Test: View {        
    var body: some View {
        VStack {
            Text("Test 1")
            Text("Test 2")
        }
    }
}

我该如何将此视图呈现为UIImage?

我已经研究了一些解决方案,例如:

extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
            layer.render(in: rendererContext.cgContext)
        }
    }
}

但似乎这样的解决方案只适用于UIView,而不是SwiftUI视图。


1
https://ericasadun.com/2019/06/20/swiftui-render-your-mojave-swiftui-views-on-the-fly/ - Josh Homann
2个回答

22

这是适合我的方法,我需要确保图片的大小与其他并排放置的图片一致。希望对他人有所帮助。

演示:上面的分隔符是 SwiftUI 渲染的,下面是图像(在边框内显示大小)

更新:使用 Xcode 13.4 / iOS 15.5 进行重新测试

项目中的测试模块在此处

“输入图片描述”/

extension View {
    func asImage() -> UIImage {
        let controller = UIHostingController(rootView: self)

        // locate far out of screen
        controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)

        let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
        controller.view.bounds = CGRect(origin: .zero, size: size)
        controller.view.sizeToFit()
        UIApplication.shared.windows.first?.rootViewController?.view.addSubview(controller.view)

        let image = controller.view.asImage()
        controller.view.removeFromSuperview()
        return image
    }
}
extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        return renderer.image { rendererContext in
// [!!] Uncomment to clip resulting image
//             rendererContext.cgContext.addPath(
//                UIBezierPath(roundedRect: bounds, cornerRadius: 20).cgPath)
//            rendererContext.cgContext.clip()

// As commented by @MaxIsom below in some cases might be needed
// to make this asynchronously, so uncomment below DispatchQueue
// if you'd same met crash
//            DispatchQueue.main.async {
                 layer.render(in: rendererContext.cgContext)
//            }
        }
    }
}


// TESTING
struct TestableView: View {
    var body: some View {
        VStack {
            Text("Test 1")
            Text("Test 2")
        }
    }
}

struct TestBackgroundRendering: View {
    var body: some View {
        VStack {
            TestableView()
            Divider()
            Image(uiImage: render())
                .border(Color.black)
        }
    }
    
    private func render() -> UIImage {
        TestableView().asImage()
    }
}

render() 返回 UIImage,而 Image(uiImage:) 则是 SwiftUI 中使用 UIImage 的准确方式。 - Asperi
2
@Asperi 显然,如果TestableView包含通过@Binding传递给它的动态数据,则无法正常工作。在UIView扩展的layer.render行中出现Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)错误。 - AnaghSharma
1
我在处理动态数据时遇到了同样的问题。@AnaghSharma,你找到解决方法了吗? - Thiago
2
通过将 self.layer.render(in: rendererContext.cgContext)DispatchQueue.main.async {} 包装起来,我成功防止了它崩溃。 - Max Isom
2
我使用 DispatchQueue.main.async { [weak self] in layer.render } 时得到了空图像,但是没有使用它时得到了正确的图像。 - zdravko zdravkin
显示剩余11条评论

3

Asperi的解决方案有效,但是如果你需要无白色背景的图像,你需要添加以下这行代码:

controller.view.backgroundColor = .clear

你的视图扩展将会是:

extension View {
    func asImage() -> UIImage {
        let controller = UIHostingController(rootView: self)
        
        // locate far out of screen
        controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
        UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
        
        let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
        controller.view.bounds = CGRect(origin: .zero, size: size)
        controller.view.sizeToFit()
        controller.view.backgroundColor = .clear
        let image = controller.view.asImage()
        controller.view.removeFromSuperview()
        return image
    }
}

如果你在 TestableView() 中添加 .backgroundColor() 修饰符,它将呈现该颜色。 - zdravko zdravkin
这个解决方案不起作用,因为我正在尝试将SwiftUI转换为Storyboard ViewController。它只显示一个简单的白色框,什么都没有。 - Arslan Kaleem

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