6

I'm currently trying to find a way to save an image from AsyncImage so I can cache it right away (without using a separate image retrieval process).

This is my code for AsyncImage and the Extension on View I'm using to try to save it.

AsyncImage(url: asyncUrl) { phase in
    
    switch phase {
    case .empty:
        ProgressView()
    case .success(let image):
        image
            .resizable()
            .scaledToFit()
        urlImageModel.image = image.snapshot()
    case .failure:
        Image(uiImage: UrlImageView.defaultImage!)
            .resizable()
            .scaledToFit()
    @unknown default:
        Image(uiImage: UrlImageView.defaultImage!)
            .resizable()
            .scaledToFit()
    }
}

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

Unfortunately, when I put the code in case .success to save the image in my urlImageModel, it gives this error:

Type () cannot conform to View in reference to the image closure.

Any ideas on how to save the image from within AsyncImage would be appreciated!

Denis
  • 3,167
  • 1
  • 22
  • 23

2 Answers2

5

The content closure you're passing to AsyncImage is a @ViewBuilder. That means that every statement that isn't a declaration (like let x = ...) or control flow (like if blah { ... }) needs to be an expression that evaluates to some View. The problem is with this statement:

urlImageModel.image = image.snapshot()

That isn't a declaration or control flow. It's an assignment. Swift treats it like it evaluates to a Void value (also written (), the empty tuple), and Void is not a View.

You can fix it by wrapping it in a declaration. But another thing to watch out for is that you should never change your model objects during SwiftUI's redisplay phase (during which it asks a View for its body). So you should also dispatch it to be safe. The dispatched block will be run later, outside of SwiftUI's redisplay phase.

let _ = DispatchQueue.main.async {
    urlImageModel.image = image.snapshot()
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
2

you could simply use this, works well for me:

 image
     .resizable()
     .scaledToFit()
     .onAppear {
         urlImageModel.image = image.snapshot()
     }