iOS: Foto schon beim Anlegen eines Produkts hinterlegen

Anlege-Formular bekommt eine Foto-Sektion (Kamera/Galerie) mit Vorschau. Das
Bild wird nach dem Anlegen hochgeladen (dann gibt es eine Produkt-ID). Die
JPEG-Aufbereitung ist jetzt eine freie Funktion (productImageJPEG), von der
Detail- und Anlege-Ansicht gemeinsam genutzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-28 10:32:49 +02:00
parent 4141781ffb
commit c67998d78b
2 changed files with 62 additions and 16 deletions

View File

@@ -196,7 +196,7 @@ struct ProductPhotoSection: View {
}
private func upload(_ image: UIImage) async {
guard let data = Self.prepared(image) else { return }
guard let data = productImageJPEG(image) else { return }
busy = true
defer { busy = false }
error = nil
@@ -223,22 +223,24 @@ struct ProductPhotoSection: View {
}
}
/// Foto verkleinern und als JPEG unter die Server-Grenze (2 MB) drücken.
private static func prepared(_ image: UIImage, maxDim: CGFloat = 1600) -> Data? {
let size = image.size
let scale = min(1, maxDim / max(size.width, size.height))
let target = CGSize(width: size.width * scale, height: size.height * scale)
let scaled = UIGraphicsImageRenderer(size: target).image { _ in
image.draw(in: CGRect(origin: .zero, size: target))
}
var quality: CGFloat = 0.8
var data = scaled.jpegData(compressionQuality: quality)
while let d = data, d.count > 1_800_000, quality > 0.3 {
quality -= 0.15
data = scaled.jpegData(compressionQuality: quality)
}
return data
}
/// Foto verkleinern und als JPEG unter die Server-Grenze (2 MB) drücken.
/// Frei (nicht an eine View gebunden), damit auch das Anlege-Formular es nutzt.
func productImageJPEG(_ image: UIImage, maxDim: CGFloat = 1600) -> Data? {
let size = image.size
let scale = min(1, maxDim / max(size.width, size.height))
let target = CGSize(width: size.width * scale, height: size.height * scale)
let scaled = UIGraphicsImageRenderer(size: target).image { _ in
image.draw(in: CGRect(origin: .zero, size: target))
}
var quality: CGFloat = 0.8
var data = scaled.jpegData(compressionQuality: quality)
while let d = data, d.count > 1_800_000, quality > 0.3 {
quality -= 0.15
data = scaled.jpegData(compressionQuality: quality)
}
return data
}
/// Kamera über UIImagePickerController SwiftUI hat keine eigene Kamera-Ansicht.

View File

@@ -1,4 +1,5 @@
import SwiftUI
import PhotosUI
/// Artikel per Namenssuche auswählen (Alternative zum Scannen).
struct ProductSearchView: View {
@@ -84,6 +85,10 @@ struct ProductFormView: View {
@State private var newGroupShown = false
@State private var busy = false
@State private var error: String?
// Foto schon beim Anlegen: erst nach dem Anlegen hochladen (ID nötig).
@State private var pendingImage: UIImage?
@State private var showCamera = false
@State private var pickerItem: PhotosPickerItem?
private let baseUnits = [("piece", "Stück"), ("gram", "Gramm"), ("milliliter", "Milliliter")]
// Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel.
@@ -174,6 +179,25 @@ struct ProductFormView: View {
}
}
}
// Foto direkt beim Anlegen z.B. wenn Open Food/Products Facts keins
// liefert. Wird nach dem Anlegen hochgeladen (dann gibt es eine ID).
Section("Foto") {
if let pendingImage {
Image(uiImage: pendingImage)
.resizable().scaledToFit()
.frame(maxWidth: .infinity).frame(maxHeight: 200)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
Button { showCamera = true } label: { Label("Foto machen", systemImage: "camera") }
PhotosPicker(selection: $pickerItem, matching: .images) {
Label("Aus Galerie", systemImage: "photo.on.rectangle")
}
if pendingImage != nil {
Button(role: .destructive) { pendingImage = nil } label: {
Label("Foto entfernen", systemImage: "trash")
}
}
}
if let error {
Section { Text(error).foregroundStyle(.red).font(.callout) }
}
@@ -184,6 +208,20 @@ struct ProductFormView: View {
}
.navigationTitle("Neuer Artikel")
.navigationBarTitleDisplayMode(.inline)
.fullScreenCover(isPresented: $showCamera) {
CameraPicker(isPresented: $showCamera) { image in pendingImage = image }
.ignoresSafeArea()
}
.onChange(of: pickerItem) { item in
guard let item else { return }
Task {
if let data = try? await item.loadTransferable(type: Data.self),
let ui = UIImage(data: data) {
pendingImage = ui
}
pickerItem = nil
}
}
.onAppear(perform: prefill)
.task {
groups = (try? await APIClient.shared.groups()) ?? []
@@ -242,6 +280,12 @@ struct ProductFormView: View {
bulk: modus == "object" ? (objMode == "bulk") : nil
)
)
// Vorgemerktes Foto hochladen ein Fehler darf das Anlegen nicht
// zunichtemachen, der Artikel existiert bereits.
if let pendingImage, let data = productImageJPEG(pendingImage) {
_ = try? await APIClient.shared.uploadProductImage(
id: product.id, data: data, contentType: "image/jpeg")
}
onCreated(product)
dismiss()
} catch {