Skip to content

Instantly share code, notes, and snippets.

@valvoline
Created February 8, 2023 18:25
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save valvoline/6cc8afd99938c6c30736ecccda3498c7 to your computer and use it in GitHub Desktop.
Save valvoline/6cc8afd99938c6c30736ecccda3498c7 to your computer and use it in GitHub Desktop.
//
// CacheAsyncImage.swift
//
// Created by Costantino Pistagna on 08/02/23.
//
import SwiftUI
struct CacheAsyncImage<Content, Content2>: View where Content: View, Content2: View {
private let url: URL?
private let scale: CGFloat
private let transaction: Transaction?
private let contentPhase: ((AsyncImagePhase) -> Content)?
private let contentImage: ((Image) -> Content)?
private let placeholder: (() -> Content2)?
init(url: URL?,
scale: CGFloat = 1.0,
transaction: Transaction = Transaction(),
@ViewBuilder content: @escaping (AsyncImagePhase) -> Content
) {
self.url = url
self.scale = scale
self.transaction = transaction
self.contentPhase = content
self.contentImage = nil
self.placeholder = nil
}
init(url: URL?,
scale: CGFloat = 1,
@ViewBuilder content: @escaping (Image) -> Content,
@ViewBuilder placeholder: @escaping () -> Content2
) {
self.url = url
self.scale = scale
self.contentImage = content
self.placeholder = placeholder
self.contentPhase = nil
self.transaction = nil
}
var body: some View {
if let cached = ImageCache[url] {
#if DEBUG
let _ = print("cached: \(url?.absoluteString ?? "")")
#endif
if contentPhase != nil {
contentPhase?(.success(cached))
} else if contentImage != nil {
contentImage?(cached)
}
} else{
#if DEBUG
let _ = print("request: \(url?.absoluteString ?? "")")
#endif
if contentPhase != nil {
AsyncImage(url: url,
scale: scale,
transaction: transaction ?? Transaction(),
content: { cacheAndRender(phase: $0) })
} else if contentImage != nil && placeholder != nil {
AsyncImage(url: url,
scale: scale,
content: { cacheAndRender(image: $0) },
placeholder: placeholder!)
}
}
}
private func cacheAndRender(image: Image) -> some View {
ImageCache[url] = image
return contentImage?(image)
}
private func cacheAndRender(phase: AsyncImagePhase) -> some View{
if case .success (let image) = phase {
ImageCache[url] = image
}
return contentPhase?(phase)
}
}
fileprivate class ImageCache{
static private var cache: [URL: Image] = [:]
static subscript(url: URL?) -> Image?{
get {
guard let url else { return nil }
return ImageCache.cache[url]
}
set {
guard let url else { return }
ImageCache.cache[url] = newValue
}
}
}
@rursache
Copy link

rursache commented Aug 29, 2023

@valvoline

The line

init(url: URL?,
        scale: CGFloat = 1.0,
        transaction: Transaction = Transaction(),
        @ViewBuilder content: @escaping (AsyncImagePhase) -> Content
    ) {

should be

init(url: URL?,
        scale: CGFloat = 1.0,
        transaction: Transaction = Transaction(),
        @ViewBuilder content: @escaping (AsyncImagePhase) -> Content
    ) where Content: View, Content2 == Never {

to prevent getting the error

Generic parameter 'Content2' could not be inferred

when doing this:

VStack() {
  CacheAsyncImage(url: URL(string: "https://placehold.co/600x400.png")) { result in
    result
      .image?
      .resizable()
  }
            
  CacheAsyncImage(url: URL(string: "https://placehold.co/600x400.png")) { image in
    image
      .resizable()
  } placeholder: {
    Rectangle()
      .foregroundColor(.red)
  }
}

from a package/external framework

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment