0

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

https://stackoverflow.com/a/64005395/15023395

https://stackoverflow.com/a/41288197/15023395

Below is taken from https://stackoverflow.com/a/59333377/12299030

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()

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

This solution helped but I don't want to add image as subview of superView

  func extractView(){
        let hostView = UIHostingController(rootView: ContentView())
        hostView.view.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor),
            hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor),
        ]
        self.view.addSubview(hostView.view)
        self.view.addConstraints(constraints)
    }

What I want to do ???

I have a struct which extends swiftUI View and I have a design in it. Now I want to convert that swiftUI View into UIImage inside ViewController of storyboard that when my screen loads and viewDidLoad() function calls then system updates image of UIImageView in story board

Here is my SwiftUI code


import SwiftUI

struct ContentView: View {
    var body: some View {
        
        ZStack(alignment: .center){
            Rectangle()
                .frame(width: 200, height: 75)
                .cornerRadius(10)
                .foregroundColor(.white)
            Circle()
                .stroke(lineWidth:5)
                .foregroundColor(.red)
                .frame(width: 75, height: 75, alignment: .leading)
                .background(
                    Image("tempimage")
                        .resizable()
                )
        }
        
        
    }
}

Arslan Kaleem
  • 1,410
  • 11
  • 25
  • your swiftui is the part of that view controller? can you please add some more info. – Raja Kishan Apr 02 '22 at 12:20
  • No I have designed swiftUI View in another project to get the desired design. When design completed which is basically ZStack then I copied that code into the project based on storyboard. Now I want to add my design in UIImageView tried extensions but none of the extension is working – Arslan Kaleem Apr 02 '22 at 12:39
  • @ArslanKaleem - show the code for your *"designed swiftUI View"* (or at least a simplified version of it) that you are trying to convert to a `UIImage` – DonMag Apr 02 '22 at 12:45

1 Answers1

2

You can do this... but not in viewDidLoad() -- you have to wait at least until viewDidLayoutSubviews().

And, the view must be added to the view hierarchy -- but it can be removed as soon as we generate the image so it's never seen "on-screen."

Note: all "result" images here use:

  • a 240 x 200 image view
  • .contentMode = .center
  • green background so we can see the frame

and we give the UIImage generate from the SwiftUI ContentView a yellow background, because we will need to address some layout quirks.

So, to generate the image and set it to a UIImageView, we can do this:

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

}

using this imageFromContentView() func:

func imageFromContentView() -> UIImage? {
    
    let swiftUIView = UIHostingController(rootView: ContentView())
    
    // 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()
    
    // 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
    
}

Result #1:

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)
        }
    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • can we also clip the shapes ??? like what if we have a star and I want to clip the image as star not like square ??? – Arslan Kaleem Apr 02 '22 at 16:20
  • @ArslanKaleem - it's a difficult to answer abstract questions. I've done very little with SwiftUI, but I was assuming you had a specific reason for integrating it with UIKit. I'd suggest a new question, with concrete details about what you're hoping to do. – DonMag Apr 02 '22 at 20:51