/ BalanceKit / AddFoodView.swift
AddFoodView.swift
1 import SwiftUI 2 import Combine 3 import UIKit 4 5 struct AddFoodView: View { 6 @Environment(\.dismiss) private var dismiss 7 @ObservedObject var dataManager: FoodDataManager 8 9 // MARK: - State Properties 10 11 // Input fields for the base 100g values 12 @State private var foodName = "" 13 @State private var baseCalories = "" 14 @State private var baseProtein = "" 15 @State private var baseCarbs = "" 16 @State private var baseFat = "" 17 18 // Quantity for scaling 19 @State private var quantity: Double = 100 20 21 // General properties 22 @State private var selectedDate = Date() 23 @State private var selectedMealType: MealType = .lunch 24 25 // Barcode Scanner related 26 @State private var isShowingScanner = false 27 @State private var scannedBarcode: String? = nil 28 private let foodFactsService = OpenFoodFactsService() 29 30 // Alerts and fetching state 31 @State private var isFetching = false 32 @State private var showingAlert = false 33 @State private var alertTitle = "" 34 @State private var alertMessage = "" 35 36 // MARK: - Computed Properties for Scaled Values 37 38 private var scaledCalories: Double { 39 (Double(baseCalories) ?? 0) * (quantity / 100.0) 40 } 41 private var scaledProtein: Double { 42 (Double(baseProtein) ?? 0) * (quantity / 100.0) 43 } 44 private var scaledCarbs: Double { 45 (Double(baseCarbs) ?? 0) * (quantity / 100.0) 46 } 47 private var scaledFat: Double { 48 (Double(baseFat) ?? 0) * (quantity / 100.0) 49 } 50 51 private var isSaveDisabled: Bool { 52 foodName.isEmpty || baseCalories.isEmpty 53 } 54 55 // MARK: - Body 56 57 var body: some View { 58 NavigationStack { 59 Form { 60 // Section for Base Values (per 100g) 61 Section(header: Text("Basiswerte (pro 100g)")) { 62 HStack { 63 TextField("Name des Lebensmittels", text: $foodName) 64 if isFetching { 65 ProgressView().padding(.leading, 5) 66 } 67 } 68 Button(action: { isShowingScanner = true }) { 69 Label("Mit Barcode-Scanner ausfüllen", systemImage: "barcode.viewfinder") 70 } 71 72 TextField("Kalorien (kcal)", text: $baseCalories).keyboardType(.decimalPad) 73 TextField("Protein (g)", text: $baseProtein).keyboardType(.decimalPad) 74 TextField("Kohlenhydrate (g)", text: $baseCarbs).keyboardType(.decimalPad) 75 TextField("Fett (g)", text: $baseFat).keyboardType(.decimalPad) 76 } 77 78 // Section for Quantity 79 Section(header: Text("Menge")) { 80 Stepper(value: $quantity, in: 0...5000, step: 50) { 81 Text("\(quantity, specifier: "%.0f") g") 82 } 83 } 84 85 // Section for Calculated Totals 86 Section(header: Text("Gesamtwerte für diese Mahlzeit")) { 87 HStack { 88 Text("Kalorien") 89 Spacer() 90 Text("\(scaledCalories, specifier: "%.0f") kcal") 91 .foregroundColor(.secondary) 92 .animation(.easeInOut(duration: 0.3), value: scaledCalories) 93 } 94 HStack { 95 Text("Protein") 96 Spacer() 97 Text("\(scaledProtein, specifier: "%.1f") g") 98 .foregroundColor(.secondary) 99 .animation(.easeInOut(duration: 0.3), value: scaledProtein) 100 } 101 HStack { 102 Text("Kohlenhydrate") 103 Spacer() 104 Text("\(scaledCarbs, specifier: "%.1f") g") 105 .foregroundColor(.secondary) 106 .animation(.easeInOut(duration: 0.3), value: scaledCarbs) 107 } 108 HStack { 109 Text("Fett") 110 Spacer() 111 Text("\(scaledFat, specifier: "%.1f") g") 112 .foregroundColor(.secondary) 113 .animation(.easeInOut(duration: 0.3), value: scaledFat) 114 } 115 } 116 117 // Section for Date and Meal Type 118 Section { 119 DatePicker("Datum", selection: $selectedDate, displayedComponents: .date) 120 Picker("Mahlzeit", selection: $selectedMealType) { 121 ForEach(MealType.allCases, id: \.self) { type in 122 Label(type.rawValue, systemImage: type.icon) 123 } 124 } 125 } 126 127 // Save Button 128 Section { 129 Button("Speichern") { 130 saveFood() 131 } 132 .disabled(isSaveDisabled) 133 } 134 } 135 .navigationTitle("Nahrungsmittel hinzufügen") 136 .navigationBarTitleDisplayMode(.large) 137 .toolbar { 138 ToolbarItem(placement: .cancellationAction) { 139 Button("Abbrechen") { dismiss() } 140 } 141 } 142 .sheet(isPresented: $isShowingScanner) { 143 BarcodeScannerView(scannedCode: $scannedBarcode) 144 } 145 .onChange(of: scannedBarcode) { oldValue, newBarcode in 146 if let barcode = newBarcode { 147 fetchProductDetails(for: barcode) 148 } 149 } 150 .alert(isPresented: $showingAlert) { 151 Alert( 152 title: Text(alertTitle), 153 message: Text(alertMessage), 154 dismissButton: .default(Text("OK")) { 155 // Haptic feedback bei Fehlern 156 let feedback = UINotificationFeedbackGenerator() 157 feedback.notificationOccurred(.error) 158 } 159 ) 160 } 161 } 162 } 163 164 // MARK: - Logic 165 166 private func fetchProductDetails(for barcode: String) { 167 isFetching = true 168 Task { 169 do { 170 let product = try await foodFactsService.fetchProduct(for: barcode) 171 172 await MainActor.run { 173 self.foodName = product.productName ?? "Unbekanntes Produkt" 174 if let nutriments = product.nutriments { 175 self.baseCalories = String(format: "%.0f", nutriments.energyKcal100G ?? 0) 176 self.baseProtein = String(format: "%.1f", nutriments.proteins100G ?? 0) 177 self.baseCarbs = String(format: "%.1f", nutriments.carbohydrates100G ?? 0) 178 self.baseFat = String(format: "%.1f", nutriments.fat100G ?? 0) 179 self.quantity = 100 // Reset to default 180 } else { 181 showAlert(title: "Keine Nährwerte", message: "Für dieses Produkt wurden keine Nährwertinformationen gefunden.") 182 } 183 isFetching = false 184 } 185 } catch { 186 await MainActor.run { 187 isFetching = false 188 handleFetchError(error, barcode: barcode) 189 } 190 } 191 } 192 } 193 194 private func saveFood() { 195 // Haptic feedback beim Speichern 196 let impact = UIImpactFeedbackGenerator(style: .light) 197 impact.impactOccurred() 198 199 dataManager.addFoodItem( 200 name: foodName, 201 calories: Int(scaledCalories.rounded()), 202 protein: scaledProtein, 203 carbs: scaledCarbs, 204 fat: scaledFat, 205 date: selectedDate, 206 mealType: selectedMealType 207 ) 208 dismiss() 209 } 210 211 private func handleFetchError(_ error: Error, barcode: String) { 212 switch error { 213 case OpenFoodFactsService.ServiceError.productNotFound: 214 showAlert(title: "Produkt nicht gefunden", message: "Der Barcode \(barcode) wurde in der Datenbank nicht gefunden. Bitte versuche es erneut oder gib die Werte manuell ein.") 215 case OpenFoodFactsService.ServiceError.noNutriments: 216 showAlert(title: "Keine Nährwerte", message: "Für dieses Produkt wurden keine Nährwertinformationen gefunden. Du kannst die Werte manuell eingeben.") 217 default: 218 showAlert(title: "Fehler", message: "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut oder gib die Werte manuell ein.") 219 } 220 } 221 222 private func showAlert(title: String, message: String) { 223 self.alertTitle = title 224 self.alertMessage = message 225 self.showingAlert = true 226 } 227 }