Payments
This commit is contained in:
162
pedi-foods/Sources/PediFoods/Components/CachedRemoteImage.swift
Normal file
162
pedi-foods/Sources/PediFoods/Components/CachedRemoteImage.swift
Normal file
@@ -0,0 +1,162 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
struct CachedRemoteImage<Placeholder: View>: View {
|
||||
let imageURL: String?
|
||||
let ttl: TimeInterval
|
||||
let placeholder: Placeholder
|
||||
|
||||
@StateObject var loader = CachedRemoteImageLoader()
|
||||
|
||||
init(
|
||||
imageURL: String?,
|
||||
ttl: TimeInterval = 6 * 60 * 60,
|
||||
@ViewBuilder placeholder: () -> Placeholder
|
||||
) {
|
||||
self.imageURL = imageURL
|
||||
self.ttl = ttl
|
||||
self.placeholder = placeholder()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if canImport(UIKit)
|
||||
if let uiImage = loader.uiImage {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
placeholder
|
||||
}
|
||||
#elseif canImport(AppKit)
|
||||
if let nsImage = loader.nsImage {
|
||||
Image(nsImage: nsImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
placeholder
|
||||
}
|
||||
#else
|
||||
if let imageURL,
|
||||
let url = URL(string: imageURL) {
|
||||
AsyncImage(url: url) { phase in
|
||||
switch phase {
|
||||
case .success(let image):
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
default:
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
} else {
|
||||
placeholder
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
loader.load(imageURL: imageURL, ttl: ttl)
|
||||
}
|
||||
.onChange(of: imageURL) { _, newValue in
|
||||
loader.load(imageURL: newValue, ttl: ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class CachedRemoteImageLoader: ObservableObject {
|
||||
#if canImport(UIKit)
|
||||
@Published var uiImage: UIImage?
|
||||
#elseif canImport(AppKit)
|
||||
@Published var nsImage: NSImage?
|
||||
#endif
|
||||
|
||||
private var currentKey: String?
|
||||
private var task: Task<Void, Never>?
|
||||
|
||||
deinit {
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
func load(imageURL: String?, ttl: TimeInterval) {
|
||||
let normalized = Self.normalizeImageSource(imageURL)
|
||||
let key = normalized ?? ""
|
||||
guard currentKey != key else { return }
|
||||
currentKey = key
|
||||
|
||||
task?.cancel()
|
||||
|
||||
#if canImport(UIKit)
|
||||
uiImage = nil
|
||||
#elseif canImport(AppKit)
|
||||
nsImage = nil
|
||||
#endif
|
||||
|
||||
guard let normalized, normalized.isEmpty == false else { return }
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
if let image = Self.imageFromDataURL(normalized) {
|
||||
#if canImport(UIKit)
|
||||
uiImage = image
|
||||
#elseif canImport(AppKit)
|
||||
nsImage = image
|
||||
#endif
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let url = URL(string: normalized) else { return }
|
||||
|
||||
task = Task { [weak self] in
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
let image = await AppImageCache.shared.image(for: url, ttl: ttl)
|
||||
guard Task.isCancelled == false else { return }
|
||||
await MainActor.run {
|
||||
#if canImport(UIKit)
|
||||
self?.uiImage = image
|
||||
#elseif canImport(AppKit)
|
||||
self?.nsImage = image
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalizeImageSource(_ value: String?) -> String? {
|
||||
guard var normalized = value?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
normalized.isEmpty == false else { return nil }
|
||||
normalized = normalized.replacingOccurrences(of: "\\/", with: "/")
|
||||
return normalized
|
||||
}
|
||||
|
||||
#if canImport(UIKit) || canImport(AppKit)
|
||||
private static func imageFromDataURL(_ source: String) -> PlatformImage? {
|
||||
let lower = source.lowercased()
|
||||
guard lower.hasPrefix("data:image"), let commaIndex = source.firstIndex(of: ",") else { return nil }
|
||||
|
||||
let header = String(source[..<commaIndex]).lowercased()
|
||||
guard header.contains(";base64") else { return nil }
|
||||
|
||||
let payloadStart = source.index(after: commaIndex)
|
||||
let payload = String(source[payloadStart...])
|
||||
.replacingOccurrences(of: "\\/", with: "/")
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.replacingOccurrences(of: "\r", with: "")
|
||||
.replacingOccurrences(of: " ", with: "")
|
||||
|
||||
guard let data = Data(base64Encoded: payload, options: [.ignoreUnknownCharacters]) else { return nil }
|
||||
#if canImport(UIKit)
|
||||
return UIImage(data: data)
|
||||
#elseif canImport(AppKit)
|
||||
return NSImage(data: data)
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user