/ BalanceKit / FoodItem.swift
FoodItem.swift
1 // 2 // FoodItem.swift 3 // BalanceKit 4 // 5 // Created by Alexander Kunau on 12.07.25. 6 // 7 8 import Foundation 9 import HealthKit 10 import OSLog 11 12 // MARK: - Data Versioning 13 struct PersistedFoodData: Codable { 14 let version: Int 15 let foodItems: [FoodItem] 16 17 static let currentVersion = 1 18 } 19 20 struct PersistedPresetsData: Codable { 21 let version: Int 22 let presets: [MealPreset] 23 24 static let currentVersion = 1 25 } 26 27 struct PersistedGoalsData: Codable { 28 let version: Int 29 let calorieGoal: Int 30 let proteinGoal: Double 31 let carbsGoal: Double 32 let fatGoal: Double 33 34 static let currentVersion = 1 35 } 36 37 // MARK: - Meal Types 38 enum MealType: String, Codable, CaseIterable { 39 case breakfast = "Frühstück" 40 case lunch = "Mittagessen" 41 case dinner = "Abendessen" 42 case snack = "Snack" 43 44 var icon: String { 45 switch self { 46 case .breakfast: return "sunrise" 47 case .lunch: return "sun.max" 48 case .dinner: return "sunset" 49 case .snack: return "cup.and.saucer" 50 } 51 } 52 } 53 54 // Struktur für ein einzelnes Nahrungsmittel 55 struct FoodItem: Identifiable, Codable { 56 var id = UUID() 57 var name: String 58 var calories: Int 59 var protein: Double 60 var carbs: Double 61 var fat: Double 62 var date: Date 63 var mealType: MealType 64 65 // Hilfsfunktionen für Filter und Gruppierung 66 var day: Date { 67 let calendar = Calendar.current 68 return calendar.startOfDay(for: date) 69 } 70 71 var month: Date { 72 let calendar = Calendar.current 73 let components = calendar.dateComponents([.year, .month], from: date) 74 return calendar.date(from: components)! 75 } 76 } 77 78 // Struktur für eine Mahlzeiten-Vorlage 79 struct MealPreset: Identifiable, Codable { 80 var id = UUID() 81 var name: String 82 var mealType: MealType 83 var foods: [PresetFoodItem] 84 85 var totalCalories: Int { 86 foods.reduce(0) { $0 + $1.calories } 87 } 88 89 var totalProtein: Double { 90 foods.reduce(0) { $0 + $1.protein } 91 } 92 93 var totalCarbs: Double { 94 foods.reduce(0) { $0 + $1.carbs } 95 } 96 97 var totalFat: Double { 98 foods.reduce(0) { $0 + $1.fat } 99 } 100 } 101 102 // Vereinfachte Nahrungsmittel-Struktur für Vorlagen 103 struct PresetFoodItem: Identifiable, Codable { 104 var id = UUID() 105 var name: String 106 var calories: Int 107 var protein: Double 108 var carbs: Double 109 var fat: Double 110 } 111 112 // Datenmanager für die Nahrungsmittel 113 class FoodDataManager: ObservableObject { 114 @Published var foodItems: [FoodItem] = [] { 115 didSet { 116 save() 117 } 118 } 119 120 @Published var mealPresets: [MealPreset] = [] { 121 didSet { 122 savePresets() 123 } 124 } 125 126 // HealthKit Integration 127 @Published var healthManager: HealthManager? 128 129 // Neue Zielwerte 130 @Published var dailyCalorieGoal: Int = 2000 { 131 didSet { 132 saveGoals() 133 } 134 } 135 136 @Published var dailyProteinGoal: Double = 100.0 { 137 didSet { 138 saveGoals() 139 } 140 } 141 142 @Published var dailyCarbsGoal: Double = 250.0 { 143 didSet { 144 saveGoals() 145 } 146 } 147 148 @Published var dailyFatGoal: Double = 70.0 { 149 didSet { 150 saveGoals() 151 } 152 } 153 154 private let saveKey = "SavedFoodItems" 155 private let presetsSaveKey = "SavedMealPresets" 156 private let goalsSaveKey = "SavedNutritionGoals" 157 158 // Error tracking 159 @Published var lastError: String? 160 161 init() { 162 load() 163 loadPresets() 164 loadGoals() 165 166 // Initialisiere HealthManager immer (auch im Simulator für UI-Tests) 167 #if DEBUG 168 Logger.persistence.debug("FoodDataManager - Initialisiere HealthManager") 169 #endif 170 healthManager = HealthManager() 171 } 172 173 // Nahrungsmittel zur Liste hinzufügen 174 func addFoodItem(name: String, calories: Int, protein: Double, carbs: Double, fat: Double, date: Date, mealType: MealType) { 175 let newItem = FoodItem(name: name, calories: calories, protein: protein, carbs: carbs, fat: fat, date: date, mealType: mealType) 176 foodItems.append(newItem) 177 178 // In HealthKit speichern, wenn verfügbar 179 healthManager?.saveFoodItem(name: name, calories: Double(calories), protein: protein, carbs: carbs, fat: fat) 180 } 181 182 // Nahrungsmittel aktualisieren 183 func updateFoodItem(id: UUID, name: String, calories: Int, protein: Double, carbs: Double, fat: Double, date: Date, mealType: MealType) { 184 if let index = foodItems.firstIndex(where: { $0.id == id }) { 185 var updatedItem = foodItems[index] 186 updatedItem.name = name 187 updatedItem.calories = calories 188 updatedItem.protein = protein 189 updatedItem.carbs = carbs 190 updatedItem.fat = fat 191 updatedItem.date = date 192 updatedItem.mealType = mealType 193 194 foodItems[index] = updatedItem 195 } 196 } 197 198 // Nahrungsmittel aus Preset zur Liste hinzufügen 199 func addFoodItemsFromPreset(_ preset: MealPreset, date: Date) { 200 for presetItem in preset.foods { 201 let newItem = FoodItem( 202 name: presetItem.name, 203 calories: presetItem.calories, 204 protein: presetItem.protein, 205 carbs: presetItem.carbs, 206 fat: presetItem.fat, 207 date: date, 208 mealType: preset.mealType 209 ) 210 foodItems.append(newItem) 211 } 212 } 213 214 // Neues Preset erstellen 215 func createPreset(name: String, mealType: MealType, foods: [PresetFoodItem]) { 216 let newPreset = MealPreset(name: name, mealType: mealType, foods: foods) 217 mealPresets.append(newPreset) 218 } 219 220 // Preset löschen 221 func deletePreset(at index: Int) { 222 mealPresets.remove(at: index) 223 } 224 225 // Preset löschen mit ID 226 func deletePreset(withId id: UUID) { 227 if let index = mealPresets.firstIndex(where: { $0.id == id }) { 228 mealPresets.remove(at: index) 229 } 230 } 231 232 // Presets nach Mahlzeitentyp filtern 233 func presets(for mealType: MealType?) -> [MealPreset] { 234 if let type = mealType { 235 return mealPresets.filter { $0.mealType == type } 236 } else { 237 return mealPresets 238 } 239 } 240 241 // Nahrungsmittel aus der Liste entfernen 242 func deleteFoodItem(at indexSet: IndexSet) { 243 foodItems.remove(atOffsets: indexSet) 244 } 245 246 // MARK: - Helper für Datumsfilterung 247 private func foodItemsInRange(from startDate: Date, to endDate: Date) -> [FoodItem] { 248 return foodItems.filter { $0.date >= startDate && $0.date < endDate } 249 } 250 251 // MARK: - Tageswerte 252 func totalCaloriesForDay(_ date: Date) -> Int { 253 let startOfDay = Calendar.current.startOfDay(for: date) 254 let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)! 255 return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.calories } 256 } 257 258 func totalProteinForDay(_ date: Date) -> Double { 259 let startOfDay = Calendar.current.startOfDay(for: date) 260 let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)! 261 return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.protein } 262 } 263 264 func totalCarbsForDay(_ date: Date) -> Double { 265 let startOfDay = Calendar.current.startOfDay(for: date) 266 let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)! 267 return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.carbs } 268 } 269 270 func totalFatForDay(_ date: Date) -> Double { 271 let startOfDay = Calendar.current.startOfDay(for: date) 272 let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)! 273 return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.fat } 274 } 275 276 // MARK: - Wochenwerte 277 func totalCaloriesForWeek(startingFrom date: Date) -> Int { 278 let calendar = Calendar.current 279 let startOfDay = calendar.startOfDay(for: date) 280 let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)! 281 return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.calories } 282 } 283 284 func totalProteinForWeek(startingFrom date: Date) -> Double { 285 let calendar = Calendar.current 286 let startOfDay = calendar.startOfDay(for: date) 287 let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)! 288 return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.protein } 289 } 290 291 func totalCarbsForWeek(startingFrom date: Date) -> Double { 292 let calendar = Calendar.current 293 let startOfDay = calendar.startOfDay(for: date) 294 let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)! 295 return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.carbs } 296 } 297 298 func totalFatForWeek(startingFrom date: Date) -> Double { 299 let calendar = Calendar.current 300 let startOfDay = calendar.startOfDay(for: date) 301 let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)! 302 return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.fat } 303 } 304 305 // MARK: - Monatswerte 306 func totalCaloriesForMonth(_ date: Date) -> Int { 307 let calendar = Calendar.current 308 let components = calendar.dateComponents([.year, .month], from: date) 309 guard let startOfMonth = calendar.date(from: components), 310 let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 } 311 return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.calories } 312 } 313 314 func totalProteinForMonth(_ date: Date) -> Double { 315 let calendar = Calendar.current 316 let components = calendar.dateComponents([.year, .month], from: date) 317 guard let startOfMonth = calendar.date(from: components), 318 let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 } 319 return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.protein } 320 } 321 322 func totalCarbsForMonth(_ date: Date) -> Double { 323 let calendar = Calendar.current 324 let components = calendar.dateComponents([.year, .month], from: date) 325 guard let startOfMonth = calendar.date(from: components), 326 let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 } 327 return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.carbs } 328 } 329 330 func totalFatForMonth(_ date: Date) -> Double { 331 let calendar = Calendar.current 332 let components = calendar.dateComponents([.year, .month], from: date) 333 guard let startOfMonth = calendar.date(from: components), 334 let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 } 335 return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.fat } 336 } 337 338 // MARK: - Food Items Filterung 339 func foodItemsForDay(_ date: Date, mealType: MealType? = nil) -> [FoodItem] { 340 let startOfDay = Calendar.current.startOfDay(for: date) 341 let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)! 342 let dayItems = foodItemsInRange(from: startOfDay, to: endOfDay) 343 344 if let type = mealType { 345 return dayItems.filter { $0.mealType == type } 346 } 347 return dayItems 348 } 349 350 // MARK: - Persistenz mit Versioning und Error Handling 351 private func save() { 352 do { 353 let persistedData = PersistedFoodData(version: PersistedFoodData.currentVersion, foodItems: foodItems) 354 let encoded = try JSONEncoder().encode(persistedData) 355 UserDefaults.standard.set(encoded, forKey: saveKey) 356 lastError = nil 357 Logger.persistence.info("✅ FoodItems erfolgreich gespeichert (Version \(PersistedFoodData.currentVersion))") 358 } catch { 359 lastError = "Fehler beim Speichern der Nahrungsmittel: \(error.localizedDescription)" 360 Logger.persistence.error("❌ Speicherfehler FoodItems: \(error)") 361 } 362 } 363 364 // Laden der Daten aus UserDefaults mit Migration 365 private func load() { 366 guard let savedItems = UserDefaults.standard.data(forKey: saveKey) else { 367 Logger.persistence.info("ℹ️ Keine gespeicherten FoodItems gefunden - starte mit leerer Liste") 368 foodItems = [] 369 return 370 } 371 372 do { 373 // Versuche zuerst, versioned data zu laden 374 let persistedData = try JSONDecoder().decode(PersistedFoodData.self, from: savedItems) 375 376 // Führe Migration durch, falls notwendig 377 foodItems = migrateFoodItems(from: persistedData.version, items: persistedData.foodItems) 378 Logger.persistence.info("✅ FoodItems geladen (Version \(persistedData.version), \(self.foodItems.count) Items)") 379 lastError = nil 380 381 } catch { 382 Logger.persistence.warning("⚠️ Versioned load fehlgeschlagen, versuche legacy format...") 383 384 // Fallback: Versuche altes Format zu laden (ohne Versioning) 385 do { 386 let legacyItems = try JSONDecoder().decode([FoodItem].self, from: savedItems) 387 388 // Validiere und migriere Legacy-Daten 389 foodItems = validateAndCleanFoodItems(legacyItems) 390 391 // Speichere sofort im neuen Format 392 save() 393 Logger.persistence.info("✅ Legacy FoodItems erfolgreich migriert (\(self.foodItems.count) Items)") 394 395 } catch { 396 lastError = "Fehler beim Laden der Nahrungsmittel: \(error.localizedDescription)" 397 Logger.persistence.error("❌ Kritischer Fehler beim Laden von FoodItems: \(error)") 398 foodItems = [] 399 } 400 } 401 } 402 403 // Migration zwischen Versionen 404 private func migrateFoodItems(from version: Int, items: [FoodItem]) -> [FoodItem] { 405 let migratedItems = items 406 407 // Aktuell Version 1, keine Migration nötig 408 // Bei zukünftigen Versionen hier Migrationslogik hinzufügen: 409 // if version < 2 { 410 // migratedItems = migrateV1ToV2(migratedItems) 411 // } 412 413 return validateAndCleanFoodItems(migratedItems) 414 } 415 416 // Validierung und Bereinigung von FoodItems 417 private func validateAndCleanFoodItems(_ items: [FoodItem]) -> [FoodItem] { 418 return items.filter { item in 419 // Validierungskriterien 420 let isValid = item.calories >= 0 && 421 item.protein >= 0 && 422 item.carbs >= 0 && 423 item.fat >= 0 && 424 !item.name.isEmpty && 425 item.date <= Date() 426 427 if !isValid { 428 print("⚠️ Invalides FoodItem gefiltert: \(item.name)") 429 } 430 431 return isValid 432 } 433 } 434 435 // Speichern der Presets in UserDefaults mit Versioning 436 private func savePresets() { 437 do { 438 let persistedData = PersistedPresetsData(version: PersistedPresetsData.currentVersion, presets: mealPresets) 439 let encoded = try JSONEncoder().encode(persistedData) 440 UserDefaults.standard.set(encoded, forKey: presetsSaveKey) 441 print("✅ Presets erfolgreich gespeichert (Version \(PersistedPresetsData.currentVersion))") 442 } catch { 443 lastError = "Fehler beim Speichern der Presets: \(error.localizedDescription)" 444 print("❌ Speicherfehler Presets: \(error)") 445 } 446 } 447 448 // Laden der Presets aus UserDefaults mit Migration 449 private func loadPresets() { 450 guard let savedPresets = UserDefaults.standard.data(forKey: presetsSaveKey) else { 451 print("ℹ️ Keine gespeicherten Presets gefunden") 452 mealPresets = [] 453 return 454 } 455 456 do { 457 let persistedData = try JSONDecoder().decode(PersistedPresetsData.self, from: savedPresets) 458 mealPresets = validateAndCleanPresets(persistedData.presets) 459 print("✅ Presets geladen (Version \(persistedData.version), \(mealPresets.count) Presets)") 460 } catch { 461 print("⚠️ Versioned load fehlgeschlagen, versuche legacy format...") 462 463 do { 464 let legacyPresets = try JSONDecoder().decode([MealPreset].self, from: savedPresets) 465 mealPresets = validateAndCleanPresets(legacyPresets) 466 savePresets() // Speichere im neuen Format 467 print("✅ Legacy Presets erfolgreich migriert (\(mealPresets.count) Presets)") 468 } catch { 469 lastError = "Fehler beim Laden der Presets: \(error.localizedDescription)" 470 print("❌ Kritischer Fehler beim Laden von Presets: \(error)") 471 mealPresets = [] 472 } 473 } 474 } 475 476 // Validierung von Presets 477 private func validateAndCleanPresets(_ presets: [MealPreset]) -> [MealPreset] { 478 return presets.filter { preset in 479 let isValid = !preset.name.isEmpty && 480 !preset.foods.isEmpty && 481 preset.foods.allSatisfy { food in 482 food.calories >= 0 && food.protein >= 0 && food.carbs >= 0 && food.fat >= 0 483 } 484 485 if !isValid { 486 print("⚠️ Invalides Preset gefiltert: \(preset.name)") 487 } 488 489 return isValid 490 } 491 } 492 493 // MARK: - Ziele Speichern mit Versioning 494 private func saveGoals() { 495 do { 496 let persistedData = PersistedGoalsData( 497 version: PersistedGoalsData.currentVersion, 498 calorieGoal: dailyCalorieGoal, 499 proteinGoal: dailyProteinGoal, 500 carbsGoal: dailyCarbsGoal, 501 fatGoal: dailyFatGoal 502 ) 503 let encoded = try JSONEncoder().encode(persistedData) 504 UserDefaults.standard.set(encoded, forKey: goalsSaveKey) 505 print("✅ Goals erfolgreich gespeichert") 506 } catch { 507 lastError = "Fehler beim Speichern der Ziele: \(error.localizedDescription)" 508 print("❌ Speicherfehler Goals: \(error)") 509 } 510 } 511 512 // Laden der Zielwerte mit Migration 513 private func loadGoals() { 514 guard let savedData = UserDefaults.standard.data(forKey: goalsSaveKey) else { 515 // Legacy: Versuche altes Dictionary-Format 516 if let savedGoals = UserDefaults.standard.dictionary(forKey: goalsSaveKey) { 517 dailyCalorieGoal = savedGoals["calories"] as? Int ?? 2000 518 dailyProteinGoal = savedGoals["protein"] as? Double ?? 100.0 519 dailyCarbsGoal = savedGoals["carbs"] as? Double ?? 250.0 520 dailyFatGoal = savedGoals["fat"] as? Double ?? 70.0 521 print("ℹ️ Legacy Goals geladen") 522 saveGoals() // Migriere zu neuem Format 523 } else { 524 print("ℹ️ Keine gespeicherten Goals gefunden - verwende Defaults") 525 } 526 return 527 } 528 529 do { 530 let persistedData = try JSONDecoder().decode(PersistedGoalsData.self, from: savedData) 531 532 // Validiere Goals 533 dailyCalorieGoal = max(0, persistedData.calorieGoal) 534 dailyProteinGoal = max(0, persistedData.proteinGoal) 535 dailyCarbsGoal = max(0, persistedData.carbsGoal) 536 dailyFatGoal = max(0, persistedData.fatGoal) 537 538 print("✅ Goals geladen (Version \(persistedData.version))") 539 } catch { 540 lastError = "Fehler beim Laden der Ziele: \(error.localizedDescription)" 541 print("❌ Fehler beim Laden von Goals: \(error)") 542 // Behalte Defaults 543 } 544 } 545 }