0

I’m trying to take a snapshot of a SwiftUI view that includes an image downloaded using WebImage from the SDWebImageSwiftUI library. However, when I call the snapshot function, the resulting image includes only the placeholder (e.g., a ProgressView), not the actual downloaded image.

Here’s my WebImage implementation:

WebImage(url: URL(string: url)) { image in
    image.resizable()
} placeholder: {
    ProgressView()
}
.aspectRatio(contentMode: .fit)

This is my snapshot function:

func snapshot<T: View>(of view: T) -> UIImage {
        let controller = UIHostingController(rootView: view)
        let hostingView = controller.view
        
        let targetSize = hostingView?.intrinsicContentSize ?? .zero
        hostingView?.bounds = CGRect(origin: .zero, size: targetSize)
        hostingView?.backgroundColor = .clear
        
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        
        return renderer.image { _ in
            hostingView?.drawHierarchy(in: hostingView!.bounds, afterScreenUpdates: true)
        }
    }

Observations:

  1. The snapshot works perfectly for other SwiftUI components.
  2. If I wait for a long time to ensure the image is downloaded, the placeholder is still included in the snapshot instead of the actual image.

Questions:

  1. Why is the downloaded image not being included in the snapshot?
  2. How can I ensure the WebImage content is fully loaded and included in the snapshot?
  3. Is there a better way to capture a snapshot of SwiftUI views that use WebImage?

Any help would be greatly appreciated! I have added a repo with reproducible code https://github.com/nauvtechnologies/sdwebimagesnapshot

The issue is the same with AsyncImage which I have used in the repo.

4
  • can you show the code where and how to use your snapshot function. In other words, show a minimal reproducible code that produces your issue, see: minimal code. Enough code and data to compile and run a simple example of your problem. Commented Jan 22 at 8:01
  • @workingdogsupportUkraine I have uploaded a repo with code github.com/nauvtechnologies/sdwebimagesnapshot Commented Jan 22 at 8:47
  • Don't know what you are doing, your repo code does not use WebImage at all. Also it is not clear why you have class SaveImageHelper: ObservableObject, you are not using observing anything, ie no @Published. Commented Jan 22 at 9:45
  • I have setup reproducible code with AsyncImage instead. Its the same thing, just try capturing a screenshot and the downloaded image wont be captured. I could setup WebImage but its the same thing and same issue. Also its reproducible code to mimic the issue, I dont think failing to observe can cause such an issue, its unrelated. Commented Jan 22 at 9:54

1 Answer 1

1

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)
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Hello, how can I tell an ImageRenderer to wait for an AsyncImage to finish loading. Thanks in advance
@Djiby ImageRenderer won’t work because it cannot “wait”. You need to host the SwiftUI view and use a UIGraphicsImageRenderer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.