iOS: Produktliste zeigt Artikelbild statt Kästchen-Symbol
Hat ein Artikel ein Bild, zeigt die Produktliste jetzt ein Vorschaubild statt des neutralen shippingbox-Symbols; ohne Bild bleibt das Symbol. Ein gemeinsamer ProductImageCache verhindert, dass beim Scrollen dieselben Bilder immer wieder geladen werden, und merkt sich auch "kein Bild". ProductPhotoSection haelt den Cache bei Laden/Upload/Entfernen synchron. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -298,8 +298,7 @@ struct ProductRow: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "shippingbox")
|
ProductThumb(productId: product.id)
|
||||||
.font(.body).foregroundStyle(.secondary).frame(width: 24)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(product.name)
|
Text(product.name)
|
||||||
Text(bestand)
|
Text(bestand)
|
||||||
|
|||||||
@@ -1,6 +1,73 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
|
|
||||||
|
/// Gemeinsamer Zwischenspeicher für Artikelbilder, damit Listen beim Scrollen
|
||||||
|
/// nicht dieselben Bilder immer wieder laden. „Kein Bild" wird ebenfalls
|
||||||
|
/// gemerkt, damit erfolglose Abfragen sich nicht wiederholen.
|
||||||
|
final class ProductImageCache {
|
||||||
|
static let shared = ProductImageCache()
|
||||||
|
private let cache = NSCache<NSNumber, UIImage>()
|
||||||
|
private var empty: Set<Int> = []
|
||||||
|
private let lock = NSLock()
|
||||||
|
|
||||||
|
func image(for id: Int) -> UIImage? { cache.object(forKey: NSNumber(value: id)) }
|
||||||
|
|
||||||
|
func isKnownEmpty(_ id: Int) -> Bool {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
return empty.contains(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func store(_ image: UIImage, for id: Int) {
|
||||||
|
cache.setObject(image, forKey: NSNumber(value: id))
|
||||||
|
lock.lock(); empty.remove(id); lock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markEmpty(_ id: Int) {
|
||||||
|
cache.removeObject(forKey: NSNumber(value: id))
|
||||||
|
lock.lock(); empty.insert(id); lock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kleines Vorschaubild für Listen: zeigt das Artikelbild, sonst das neutrale
|
||||||
|
/// Kästchen-Symbol wie bisher. Ergebnisse kommen aus dem gemeinsamen Cache.
|
||||||
|
struct ProductThumb: View {
|
||||||
|
let productId: Int
|
||||||
|
@State private var image: UIImage?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.task(id: productId) { await load() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var content: some View {
|
||||||
|
if let image {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable().scaledToFill()
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||||
|
} else {
|
||||||
|
Image(systemName: "shippingbox")
|
||||||
|
.font(.body).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() async {
|
||||||
|
if let cached = ProductImageCache.shared.image(for: productId) {
|
||||||
|
image = cached
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ProductImageCache.shared.isKnownEmpty(productId) { return }
|
||||||
|
guard let data = try? await APIClient.shared.productImage(id: productId),
|
||||||
|
let ui = UIImage(data: data) else {
|
||||||
|
ProductImageCache.shared.markEmpty(productId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ProductImageCache.shared.store(ui, for: productId)
|
||||||
|
image = ui
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Artikelfoto ansehen, per Kamera aufnehmen oder aus der Galerie wählen und
|
/// Artikelfoto ansehen, per Kamera aufnehmen oder aus der Galerie wählen und
|
||||||
/// hochladen. Wird als Abschnitt in die Produkt-Detailansicht eingebettet.
|
/// hochladen. Wird als Abschnitt in die Produkt-Detailansicht eingebettet.
|
||||||
struct ProductPhotoSection: View {
|
struct ProductPhotoSection: View {
|
||||||
@@ -58,6 +125,12 @@ struct ProductPhotoSection: View {
|
|||||||
|
|
||||||
private func load() async {
|
private func load() async {
|
||||||
imageData = try? await APIClient.shared.productImage(id: productId)
|
imageData = try? await APIClient.shared.productImage(id: productId)
|
||||||
|
// Listen-Cache mit der Wahrheit abgleichen.
|
||||||
|
if let imageData, let ui = UIImage(data: imageData) {
|
||||||
|
ProductImageCache.shared.store(ui, for: productId)
|
||||||
|
} else {
|
||||||
|
ProductImageCache.shared.markEmpty(productId)
|
||||||
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +143,7 @@ struct ProductPhotoSection: View {
|
|||||||
_ = try await APIClient.shared.uploadProductImage(
|
_ = try await APIClient.shared.uploadProductImage(
|
||||||
id: productId, data: data, contentType: "image/jpeg")
|
id: productId, data: data, contentType: "image/jpeg")
|
||||||
imageData = data
|
imageData = data
|
||||||
|
if let ui = UIImage(data: data) { ProductImageCache.shared.store(ui, for: productId) }
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
}
|
}
|
||||||
@@ -82,6 +156,7 @@ struct ProductPhotoSection: View {
|
|||||||
do {
|
do {
|
||||||
try await APIClient.shared.deleteProductImage(id: productId)
|
try await APIClient.shared.deleteProductImage(id: productId)
|
||||||
imageData = nil
|
imageData = nil
|
||||||
|
ProductImageCache.shared.markEmpty(productId)
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error.localizedDescription
|
self.error = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user