From 3f47e273adc3dabb8a7282a1759b87b992611a3a Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 26 Jul 2026 14:40:51 +0200 Subject: [PATCH] =?UTF-8?q?iOS:=20Produktliste=20zeigt=20Artikelbild=20sta?= =?UTF-8?q?tt=20K=C3=A4stchen-Symbol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ios/Sources/ListViews.swift | 3 +- ios/Sources/ProductPhotoView.swift | 75 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/ios/Sources/ListViews.swift b/ios/Sources/ListViews.swift index b2fbd64..c6bf8f6 100644 --- a/ios/Sources/ListViews.swift +++ b/ios/Sources/ListViews.swift @@ -298,8 +298,7 @@ struct ProductRow: View { var body: some View { HStack(spacing: 12) { - Image(systemName: "shippingbox") - .font(.body).foregroundStyle(.secondary).frame(width: 24) + ProductThumb(productId: product.id) VStack(alignment: .leading, spacing: 2) { Text(product.name) Text(bestand) diff --git a/ios/Sources/ProductPhotoView.swift b/ios/Sources/ProductPhotoView.swift index 63c1231..60ff647 100644 --- a/ios/Sources/ProductPhotoView.swift +++ b/ios/Sources/ProductPhotoView.swift @@ -1,6 +1,73 @@ import SwiftUI 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() + private var empty: Set = [] + 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 /// hochladen. Wird als Abschnitt in die Produkt-Detailansicht eingebettet. struct ProductPhotoSection: View { @@ -58,6 +125,12 @@ struct ProductPhotoSection: View { private func load() async { 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 } @@ -70,6 +143,7 @@ struct ProductPhotoSection: View { _ = try await APIClient.shared.uploadProductImage( id: productId, data: data, contentType: "image/jpeg") imageData = data + if let ui = UIImage(data: data) { ProductImageCache.shared.store(ui, for: productId) } } catch { self.error = error.localizedDescription } @@ -82,6 +156,7 @@ struct ProductPhotoSection: View { do { try await APIClient.shared.deleteProductImage(id: productId) imageData = nil + ProductImageCache.shared.markEmpty(productId) } catch { self.error = error.localizedDescription }