我正在尝试拍摄 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)
}
}
观察结果:
问题:
任何帮助将不胜感激!我添加了一个带有可重现代码的存储库https://github.com/nauvtechnologies/sdwebimagesnapshot
问题与我在存储库中使用的 AsyncImage 相同。
您说您“等待了很长一段时间”才下载图像,但如果您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)
}
}
}
}