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:
@@ -196,7 +196,7 @@ struct ProductPhotoSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func upload(_ image: UIImage) async {
|
private func upload(_ image: UIImage) async {
|
||||||
guard let data = Self.prepared(image) else { return }
|
guard let data = productImageJPEG(image) else { return }
|
||||||
busy = true
|
busy = true
|
||||||
defer { busy = false }
|
defer { busy = false }
|
||||||
error = nil
|
error = nil
|
||||||
@@ -223,8 +223,11 @@ 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? {
|
|
||||||
|
/// 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 size = image.size
|
||||||
let scale = min(1, maxDim / max(size.width, size.height))
|
let scale = min(1, maxDim / max(size.width, size.height))
|
||||||
let target = CGSize(width: size.width * scale, height: size.height * scale)
|
let target = CGSize(width: size.width * scale, height: size.height * scale)
|
||||||
@@ -238,7 +241,6 @@ struct ProductPhotoSection: View {
|
|||||||
data = scaled.jpegData(compressionQuality: quality)
|
data = scaled.jpegData(compressionQuality: quality)
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kamera über UIImagePickerController – SwiftUI hat keine eigene Kamera-Ansicht.
|
/// Kamera über UIImagePickerController – SwiftUI hat keine eigene Kamera-Ansicht.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
|
||||||
/// Artikel per Namenssuche auswählen (Alternative zum Scannen).
|
/// Artikel per Namenssuche auswählen (Alternative zum Scannen).
|
||||||
struct ProductSearchView: View {
|
struct ProductSearchView: View {
|
||||||
@@ -84,6 +85,10 @@ struct ProductFormView: View {
|
|||||||
@State private var newGroupShown = false
|
@State private var newGroupShown = false
|
||||||
@State private var busy = false
|
@State private var busy = false
|
||||||
@State private var error: String?
|
@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")]
|
private let baseUnits = [("piece", "Stück"), ("gram", "Gramm"), ("milliliter", "Milliliter")]
|
||||||
// Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel.
|
// 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 {
|
if let error {
|
||||||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||||||
}
|
}
|
||||||
@@ -184,6 +208,20 @@ struct ProductFormView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle("Neuer Artikel")
|
.navigationTitle("Neuer Artikel")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.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)
|
.onAppear(perform: prefill)
|
||||||
.task {
|
.task {
|
||||||
groups = (try? await APIClient.shared.groups()) ?? []
|
groups = (try? await APIClient.shared.groups()) ?? []
|
||||||
@@ -242,6 +280,12 @@ struct ProductFormView: View {
|
|||||||
bulk: modus == "object" ? (objMode == "bulk") : nil
|
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)
|
onCreated(product)
|
||||||
dismiss()
|
dismiss()
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user