You say you "waited for a long time" for the image to be downloaded, but if you did do that correctly, you would not have this problem in the first place.
If you just have a UIHostingController and just call drawHierarchy, you are not waiting at all. drawHierarchy will cause SwiftUI will only run its lifecycle for a very short time, just enough that something is drawn.
To actually wait for the image to be downloaded, you need to add the UIHostingController to a UIWindow, and only then can the SwiftUI lifecycle run for an extended period of time. If you do a Task.sleep during this time, you can wait for the image to be downloaded.
Here is some code that does this. This is modified from ViewHosting.swift in ViewInspector. You probably can further simplify this depending on your needs.
@MainActor
public enum ViewHosting { }
public extension ViewHosting {
struct ViewId: Hashable, Sendable {
let function: String
var key: String { function }
}
@MainActor
static func host<V, R>(_ view: V,
function: String = #function,
whileHosted: @MainActor (UIViewController) async throws -> R
) async rethrows -> R where V: View {
let viewId = ViewId(function: function)
let vc = host(view: view, viewId: viewId)
let result = try await whileHosted(vc)
expel(viewId: viewId)
return result
}
@MainActor
private static func host<V>(view: V, viewId: ViewId) -> UIViewController where V: View {
let parentVC = rootViewController
let childVC = hostVC(view)
store(Hosted(viewController: childVC), viewId: viewId)
childVC.view.translatesAutoresizingMaskIntoConstraints = false
childVC.view.frame = parentVC.view.frame
willMove(childVC, to: parentVC)
parentVC.addChild(childVC)
parentVC.view.addSubview(childVC.view)
NSLayoutConstraint.activate([
childVC.view.leadingAnchor.constraint(equalTo: parentVC.view.leadingAnchor),
childVC.view.topAnchor.constraint(equalTo: parentVC.view.topAnchor),
])
didMove(childVC, to: parentVC)
window.layoutIfNeeded()
return childVC
}
static func expel(function: String = #function) {
let viewId = ViewId(function: function)
MainActor.assumeIsolated {
expel(viewId: viewId)
}
}
@MainActor
private static func expel(viewId: ViewId) {
guard let hosted = expelHosted(viewId: viewId) else { return }
let childVC = hosted.viewController
willMove(childVC, to: nil)
childVC.view.removeFromSuperview()
childVC.removeFromParent()
didMove(childVC, to: nil)
}
}
@MainActor
private extension ViewHosting {
struct Hosted {
let viewController: UIViewController
}
private static var hosted: [ViewId: Hosted] = [:]
static let window: UIWindow = makeWindow()
static func makeWindow() -> UIWindow {
let frame = UIScreen.main.bounds
let window = UIWindow(frame: frame)
installRootViewController(window)
window.makeKeyAndVisible()
window.layoutIfNeeded()
return window
}
@discardableResult
static func installRootViewController(_ window: UIWindow) -> UIViewController {
let vc = UIViewController()
window.rootViewController = vc
vc.view.translatesAutoresizingMaskIntoConstraints = false
return vc
}
static var rootViewController: UIViewController {
window.rootViewController ?? installRootViewController(window)
}
static func hostVC<V>(_ view: V) -> UIHostingController<V> where V: View {
UIHostingController(rootView: view)
}
// MARK: - WillMove & DidMove
static func willMove(_ child: UIViewController, to parent: UIViewController?) {
child.willMove(toParent: parent)
}
static func didMove(_ child: UIViewController, to parent: UIViewController?) {
child.didMove(toParent: parent)
}
// MARK: - ViewController identification
static func store(_ hosted: Hosted, viewId: ViewId) {
self.hosted[viewId] = hosted
}
static func expelHosted(viewId: ViewId) -> Hosted? {
return hosted.removeValue(forKey: viewId)
}
}
private extension NSLayoutConstraint {
func priority(_ value: UILayoutPriority) -> NSLayoutConstraint {
priority = value
return self
}
}
Here is an example usage:
struct ContentView: View {
@State private var img: UIImage?
var body: some View {
Group {
if let img {
Image(uiImage: img)
} else {
Text("Waiting...")
}
}.task {
try? await Task.sleep(for: .seconds(1))
print("Begin snapshot")
img = await snapshot(of: WebImage(url: URL(string: "https://picsum.photos/200/300"), content: \.self) {
ProgressView()
})
}
}
func snapshot(of view: some View) async -> UIImage {
await ViewHosting.host(view) { vc in
try? await Task.sleep(for: .seconds(2)) // wait for the image to download
vc.view.sizeToFit() // resize the view to be an appropriate size
let renderer = UIGraphicsImageRenderer(size: vc.view.bounds.size)
return renderer.image { _ in
vc.view.drawHierarchy(in: vc.view.bounds, afterScreenUpdates: true)
}
}
}
}
WebImageat all. Also it is not clear why you haveclass SaveImageHelper: ObservableObject, you are not using observing anything, ie no@Published.