12

Suppose I have a simple SwiftUI View that is not the ContentView such as this:

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

How can I render this view as a UIImage?

I've looked into solutions such as :

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

But it seems that solutions like that only work on UIView, not a SwiftUI View.

1

2 Answers 2

21

Here is the approach that works for me, as I needed to get image exactly sized as it is when placed alongside others. Hope it would be helpful for some else.

Demo: above divider is SwiftUI rendered, below is image (in border to show size)

Update: re-tested with Xcode 13.4 / iOS 15.5

Test module in project is here

enter image description here

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()
    }
}
Sign up to request clarification or add additional context in comments.

16 Comments

It works! Now if I needed specifically to use a UIImage type inside my view instead of Image would that be possible? Since in the code here we have Image(uiImage: render()), is there an equivalent UIImage expression with the render? And can we even use UIImage in a SwiftUI View?
@Asperi Apparently it does not work if TestableView contains dynamic data which is passed down to it using @Binding. Getting Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) in layer.render line of UIView extension.
I'm having the same issue with dynamic data. @AnaghSharma did you find a way to solve it?
I was able to prevent it from crashing by wrapping self.layer.render(in: rendererContext.cgContext) with DispatchQueue.main.async {}
I get empty image with DispatchQueue.main.async { [weak self] in layer.render }, but correct image without
|
3

Solution of Asperi works, but if you need image without white background you have to add this line:

controller.view.backgroundColor = .clear

And your View extension will be:

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

2 Comments

If you add modifier as .backgroundColor() to TestableView() will present that color.
this solution is not working as I am trying to convert swiftUI in storyboard ViewController. It is showing simply white box nothing else

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.