Nuke icon indicating copy to clipboard operation
Nuke copied to clipboard

LazyImage(url:) does not load image when using ImageRenderer

Open mergesort opened this issue 2 years ago • 4 comments

Hey @kean, thank you so much for this library, it's been a true life-saver!

I noticed in this changelog entry you mention that LazyImage should render an image when using ImageRenderer, but when I try to use ImageRenderer I'm not seeing this work.

In the screenshot below the [C] and the empty box on the bottom right are powered by LazyImage, code that looks approximately like this. (The boxed [C] is a static letter drawn when an image can't be loaded.)

extension LinkView {
    struct ImageView: View {
        let imageURL: URL

        var body: some View {
            LazyImage(url: imageURL)
                .animation(ImagePipeline.shared.cache.containsData(for: ImageRequest(url: imageURL)) ? nil : .easeIn(duration: 0.1))
        }
    }
}

Easy Vegetarian Recipes

The code I have for ImageRenderer is relatively simple but I've noticed I have the same issue in widgets, so perhaps I'm missing some sort of configuration that will load the image correctly.

@MainActor
func render(link: RichLink, width: CGFloat) -> UIImage? {
    let linkView = LinkView(link: link, annotationsDisplayStyle: .minimal)

    let renderer = ImageRenderer(content: linkView)
    renderer.scale = 3.0

    return renderer.uiImage
}

I have three questions, two of which pertain to this problem directly:

  1. Do you see anything I'm doing wrong with regards to loading images in ImageRenderer (or widgets)?
  2. Is there a way to force images to load synchronously, and would that bypass the issue?
  3. What is the best way to only animate images in place when the image is not already cached? That last one is unrelated but I noticed it in the code snippet above.

Just wanted to say thank you so much again, Nuke really has made my life a lot easier!

mergesort avatar Feb 29 '24 23:02 mergesort

Hey, @mergesort 👋

when using ImageRenderer, but when I try to use ImageRenderer I'm not seeing this work.

Can you please confirm if you see the same behavior when the image is available in the memory cache?

If there is an issue even in that scenario, it might be related to the changes introduced in 12.1.3 that temporarily removed the memory cache lookup from the first body call and haven't yet gotten to refactoring it to bring it back. I'm considering switching to .task for loading images and also adding memory cache lookup somewhere in the init.

Do you see anything I'm doing wrong with regards to loading images in ImageRenderer (or widgets)? What is the best way to only animate images in place when the image is not already cached? That last one is unrelated but I noticed it in the code snippet above.

This part is a bit inefficient:

.animation(ImagePipeline.shared.cache.containsData(for: ImageRequest(url: imageURL)) ? nil : .easeIn(duration: 0.1))

LazyImage init has a transaction parameter that will run animations if the image is loaded not from the memory cache. It's a slightly different behavior, but I would suggest giving it a try or disabling animations altogether, which is my personal preference.

Is there a way to force images to load synchronously, and would that bypass the issue?

Not really, but LazyImage has an onCompletion closure that you might find useful.

kean avatar Mar 05 '24 03:03 kean

Can you please confirm if you see the same behavior when the image is available in the memory cache?

Hi @kean @mergesort 👋

I'm experiencing the same issue. I can confirm that it persists even when the image is available in the memory cache. In my case, the images are loaded on the initial screen. Subsequently, in a child view, an ImageRenderer renders a view with these images.

I'm uncertain if it's a bug because ImageRenderer exhibits similar behavior when used with AsyncImage.

Since these images are already loaded at the time ImageRenderer renders a view, I figured I could load those images from cache, but I'm stuck because the imageTask runs asynchronously.

dankimio avatar Mar 25 '24 20:03 dankimio

I had some success with this by storing the rendered image in a @State var and updating it in onCompletion:

@State private var renderedImage: Image?

var body: some View {
    // ...
}

@MainActor
func updateRenderedImage() {
    let renderer = ImageRenderer(content: body)
    renderer.scale = displayScale
    renderedImage = renderer.uiImage.flatMap(Image.init)
}

// usage
LazyImage(url: url) { state in
    if let image = state.image {
        image
            .resizable()
            .aspectRatio(contentMode: .fit)
    } else {
        Color.blue
    }
}
.onCompletion { _ in
    Task { @MainActor in
        updateRenderedImage()
    }
}

justinswart avatar May 07 '24 17:05 justinswart

Did you guys find any solution?

chichkanov avatar Jun 05 '24 19:06 chichkanov