为什么 SwiftUI 中的 snapshot() 无法捕获从 WebImage (SDWebImageSwiftUI) 下载的图像?

问题描述 投票:0回答:1

我正在尝试拍摄 SwiftUI 视图的快照,其中包含使用 WebImage 从 SDWebImageSwiftUI 库下载的图像。但是,当我调用快照函数时,生成的图像仅包含占位符(例如 ProgressView),而不包含实际下载的图像。

这是我的 WebImage 实现:

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

这是我的快照功能:

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

观察结果:

  1. 该快照适用于其他 SwiftUI 组件。
  2. 如果我等待很长时间才能确保图像下载完毕,占位符仍然包含在快照中,而不是实际图像中。

问题:

  1. 为什么下载的图片没有包含在快照中?
  2. 如何确保 WebImage 内容完全加载并包含在快照中?
  3. 是否有更好的方法来捕获使用 WebImage 的 SwiftUI 视图的快照?

任何帮助将不胜感激!我添加了一个带有可重现代码的存储库https://github.com/nauvtechnologies/sdwebimagesnapshot

问题与我在存储库中使用的 AsyncImage 相同。

ios swift swiftui sdwebimage sdwebimageswiftui
1个回答
0
投票

您说您“等待了很长一段时间”才下载图像,但如果您did正确地执行了该操作,那么您一开始就不会遇到此问题。

如果您只有

UIHostingController
并且只需拨打
drawHierarchy
,那么您根本不需要等待。
drawHierarchy
将导致 SwiftUI 只会运行其生命周期很短的时间,足以绘制一些东西。

要真正等待图像下载,您需要将

UIHostingController
添加到
UIWindow
,只有这样 SwiftUI 生命周期才能长时间运行。如果您在此期间执行
Task.sleep
,则可以等待图像下载。

这是执行此操作的一些代码。这是从 ViewInspector 中的 ViewHosting.swift 修改的。您可能可以根据您的需要进一步简化。

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

这是一个用法示例:

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)
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.