iOS 16 Update
Note: iOS 16 has ImageRenderer now see https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image . I do not know if it has the same issues but the below code should work with versions of iOS before 16.
Original Post
The first failure (where the view does not render at all) is because the drawHierarchy fails, seemingly because it isn't run after onAppear (I think).
The second one is because the UIHostingController introduces safe area inset padding (see Cannot place SwiftUI view outside the SafeArea when embedded in UIHostingController )
Here is a sample image with a working example (see code below):

Here is an example code with the last one working:
import SwiftUI
struct ContentView: View {
// normal view version using Text
@State
var textView: AnyView
// uiImage created before onAppear
@State
var imageBeforeOnAppear: UIImage?
// uiImage displayed using image without safe area insets
@State
var imageWithoutSafeArea: UIImage?
// uiImage displayed using image with safe area insets
@State
var imageWithSafeArea: UIImage?
@State
var show = false
var body: some View{
VStack{
if (!show){
Button(action: {
self.render()
self.show.toggle()
}, label: {Text("Show")})
}
if (show){
Text("Normal View")
self.textView.border(Color.red, width: 5)
Text("Image created in init (drawHierarchy fails)")
Image(uiImage: self.imageBeforeOnAppear!).border(Color.red, width: 5)
Text("Image created without SafeArea being disabled")
Image(uiImage: self.imageWithoutSafeArea!).border(Color.red, width: 5)
Text("Image created with SafeArea being disabled")
Image(uiImage: self.imageWithSafeArea!).border(Color.red, width: 5)
}
}
}
init(){
print("Creating content view...")
// generate a nice looking date for our text
let localDateFormatter = DateFormatter();
localDateFormatter.dateStyle = DateFormatter.Style.short
localDateFormatter.timeStyle = DateFormatter.Style.medium
localDateFormatter.string(from: Date())
let textString = "Generated at \(localDateFormatter.string(from: Date()))"
print(textString)
self.textView = AnyView(Text(textString)
.foregroundColor(Color.white)
.padding()
.background(Color.purple))
let iboa = ContentView.toImage(view: textView, disableSafeArea: true)
print("Image size \(iboa.size)")
// see https://stackoverflow.com/questions/56691630/swiftui-state-var-initialization-issue
// force this to be initialized in init
// this will NOT work:
// self.imageBeforeOnAppear = iboa
// it will still be null
self._imageBeforeOnAppear = State(initialValue: iboa)
}
// Need to call this after the ContentView is showing!
// That's why we have a button to delay this (and not
// run it on init for example) otherwise drawHierarchy
// will fail if you set afterScreenUpdates to true!
// (and it won't render anything if you set afterScreenUpdates
// to false)
func render(){
// generate image version with safe area disabled
self.imageWithSafeArea = ContentView.toImage(view: textView)
print("Image size \(self.imageWithSafeArea!.size)")
// generate image version without safe area disabled
self.imageWithoutSafeArea = ContentView.toImage(view: textView, disableSafeArea: false)
print("Image size \(self.imageWithoutSafeArea!.size)")
}
static func toImage(view: AnyView, disableSafeArea: Bool = true) -> UIImage{
print("Thread info \(Thread.current)")
print("Thread main? \(Thread.current.isMainThread)")
let controller = UIHostingController(rootView:view)
if (disableSafeArea){
// otherwise there is a buffer at the top of the frame
controller.disableSafeArea()
}
controller.view.setNeedsLayout()
controller.view.layoutIfNeeded()
let targetSize = controller.view.intrinsicContentSize
print("Image Target Size \(targetSize)")
let rect = CGRect(x: 0,
y: 0,
width: targetSize.width,
height:targetSize.height)
controller.view.bounds = rect
controller.view.frame = rect
// so we at least see something if the SwiftUI view
// fails to render
controller.view.backgroundColor = UIColor.green
let renderer = UIGraphicsImageRenderer(size: targetSize)
let image = renderer.image(actions: { rendererContext in
controller.view.layer.render(in: rendererContext.cgContext)
let result = controller.view.drawHierarchy(in: controller.view.bounds,
afterScreenUpdates: true)
print("drawHierarchy successful? \(result)")
})
return image
}
}
// see https://stackoverflow.com/questions/70156299/cannot-place-swiftui-view-outside-the-safearea-when-embedded-in-uihostingcontrol
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)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}