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