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:
Scarriffle
2026-07-26 14:40:51 +02:00
parent 272609e70a
commit 3f47e273ad
2 changed files with 76 additions and 2 deletions

View File

@@ -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)

View File

@@ -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
} }