Home > Net >  How to convert SwiftUI View body to UIImage in ViewController
How to convert SwiftUI View body to UIImage in ViewController

Time:04-03

I am working on this convertion and tried many solutions (extensions and methods) as there are so many questions and answers related to this but nothing helped like I have tried following solutions but didn't helped

Tried Solutions

enter image description here

Notice the 20-pt yellow band at the top, and the content is not vertically centered... that's because the UIHostingController applies a safe area layout guide.

Couple options to get around that...

If we add this line:

    view.addSubview(v)
    swiftUIView.didMove(toParent: self)

    // add same bottom safe area inset as top
    swiftUIView.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: v.safeAreaInsets.top, right: 0)
    
    // size the view to its content
    v.sizeToFit()

we get this result:

enter image description here

the rendered image now has 20-pts Top and Bottom "safe area" insets.

If we don't want any safe area insets, we can use this extension:

// extension to remove safe area from UIHostingController
//  source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
    convenience public init(rootView: Content, ignoreSafeArea: Bool) {
        self.init(rootView: rootView)
        
        if ignoreSafeArea {
            disableSafeArea()
        }
    }
    
    func disableSafeArea() {
        guard let viewClass = object_getClass(view) else { return }
        
        let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
        if let viewSubclass = NSClassFromString(viewSubclassName) {
            object_setClass(view, viewSubclass)
        }
        else {
            guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
            guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
            
            if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
                let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
                    return .zero
                }
                class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
            }
            
            objc_registerClassPair(viewSubclass)
            object_setClass(view, viewSubclass)
        }
    }
}

and change the first line in our func to:

let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)

and we get this result:

enter image description here

Because the SwiftUI ContentView layout is using a zStack where its content (the "ring") exceeds its vertical bounds, the top and bottom of the ring is "clipped."

We can fix that either by changing the framing in ContentView:

enter image description here

or by increasing the frame height of the loaded view, like this for example:

    // size the view to its content
    v.sizeToFit()
    
    // for this explicit example, the "ring" extends vertically
    //  outside the bounds of the zStack
    //  so we'll add 10-pts height
    v.frame.size.height  = 10.0
    

enter image description here


Here's a complete implementation (using your unmodified ContentView):

class ViewController: UIViewController {

    let imgView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        imgView.contentMode = .center
        
        imgView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(imgView)

        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            // let's put the imageView 40-pts from Top
            imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            // centered horizontally
            imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            // width: 240
            imgView.widthAnchor.constraint(equalToConstant: 240.0),
            // height: 200
            imgView.heightAnchor.constraint(equalToConstant: 200.0),
        ])

        // show the image view background so we
        //  can see its frame
        imgView.backgroundColor = .systemGreen

    }

    // we will generate the image in viewDidLayoutSubview()
    //  but that can be (and usually is) called more than once
    //  so we'll use this to make sure we only generate the image once
    var firstTime: Bool = true
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // we only want this to run once
        if firstTime {
            firstTime = false
            if let img = imageFromContentView() {
                imgView.image = img
            }
        }
    
    }

    func imageFromContentView() -> UIImage? {
        
        let swiftUIView = UIHostingController(rootView: ContentView(), ignoreSafeArea: true)
        
        // add as chlld controller
        addChild(swiftUIView)
        
        // make sure we can get its view (safely unwrap its view)
        guard let v = swiftUIView.view else {
            swiftUIView.willMove(toParent: nil)
            swiftUIView.removeFromParent()
            return nil
        }
        
        view.addSubview(v)
        swiftUIView.didMove(toParent: self)
        
        // size the view to its content
        v.sizeToFit()
        
        // for this explicit example, the "ring" extends vertically
        //  outside the bounds of the zStack
        //  so we'll add 10-pts height
        v.frame.size.height  = 10.0
        
        // force it to layout its subviews
        v.setNeedsLayout()
        v.layoutIfNeeded()
        
        // if we want to see the background
        v.backgroundColor = .systemYellow
        
        // get it as a UIImage
        let img = v.asImage()
        
        // we're done with it, so get rid of it
        v.removeFromSuperview()
        swiftUIView.willMove(toParent: nil)
        swiftUIView.removeFromParent()
        
        return img
        
    }
}

// extension to remove safe area from UIHostingController
//  source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
    convenience public init(rootView: Content, ignoreSafeArea: Bool) {
        self.init(rootView: rootView)
        
        if ignoreSafeArea {
            disableSafeArea()
        }
    }
    
    func disableSafeArea() {
        guard let viewClass = object_getClass(view) else { return }
        
        let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
        if let viewSubclass = NSClassFromString(viewSubclassName) {
            object_setClass(view, viewSubclass)
        }
        else {
            guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
            guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
            
            if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
                let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
                    return .zero
                }
                class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
            }
            
            objc_registerClassPair(viewSubclass)
            object_setClass(view, viewSubclass)
        }
    }
}

extension UIView {
    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(size: frame.size)
        return renderer.image { context in
            layer.render(in: context.cgContext)
        }
    }
}
  • Related