/ BalanceKit / UserProfile.swift
UserProfile.swift
1 // 2 // UserProfile.swift 3 // BalanceKit 4 // 5 // Created by Alexander Kunau on 12.07.25. 6 // 7 8 import Foundation 9 10 // MARK: - Data Versioning 11 struct PersistedUserProfile: Codable { 12 let version: Int 13 let profile: UserProfileData 14 15 static let currentVersion = 1 16 } 17 18 struct UserProfileData: Codable { 19 var isOnboardingCompleted: Bool 20 var age: Int 21 var gender: Gender 22 var height: Double 23 var weight: Double 24 var targetWeight: Double 25 var activityLevel: ActivityLevel 26 var weightGoal: WeightGoal 27 } 28 29 // MARK: - Enums 30 enum Gender: String, CaseIterable, Identifiable, Codable { 31 case male = "Männlich" 32 case female = "Weiblich" 33 34 var id: String { self.rawValue } 35 } 36 37 enum ActivityLevel: String, CaseIterable, Identifiable, Codable { 38 case sedentary = "Inaktiv (Bürojob, kaum Bewegung)" 39 case lightlyActive = "Leicht aktiv (1-3x Sport pro Woche)" 40 case moderatelyActive = "Moderat aktiv (3-5x Sport pro Woche)" 41 case veryActive = "Sehr aktiv (6-7x Sport pro Woche)" 42 case extremelyActive = "Extrem aktiv (Profi-Sportler, mehrmals täglich Training)" 43 44 var id: String { self.rawValue } 45 46 var factor: Double { 47 switch self { 48 case .sedentary: 49 return 1.2 50 case .lightlyActive: 51 return 1.375 52 case .moderatelyActive: 53 return 1.55 54 case .veryActive: 55 return 1.725 56 case .extremelyActive: 57 return 1.9 58 } 59 } 60 } 61 62 enum WeightGoal: String, CaseIterable, Identifiable, Codable { 63 case lose = "Abnehmen" 64 case maintain = "Gewicht halten" 65 case gain = "Zunehmen" 66 67 var id: String { self.rawValue } 68 69 var calorieAdjustment: Double { 70 switch self { 71 case .lose: 72 return 0.8 // 20% Kaloriendefizit 73 case .maintain: 74 return 1.0 // Keine Anpassung 75 case .gain: 76 return 1.1 // 10% Kalorienüberschuss 77 } 78 } 79 80 // Makronährstoff-Verteilungen basierend auf dem Ziel 81 var macroRatio: (protein: Double, carbs: Double, fat: Double) { 82 switch self { 83 case .lose: 84 return (0.35, 0.4, 0.25) // Höheres Protein für Muskelerhalt 85 case .maintain: 86 return (0.3, 0.45, 0.25) // Ausgewogen 87 case .gain: 88 return (0.25, 0.5, 0.25) // Höhere Kohlenhydrate für Energie 89 } 90 } 91 } 92 93 class UserProfile: ObservableObject, Codable { 94 @Published var isOnboardingCompleted: Bool = false 95 @Published var age: Int = 30 96 @Published var gender: Gender = .male 97 @Published var height: Double = 175 // in cm 98 @Published var weight: Double = 75 // in kg 99 @Published var targetWeight: Double = 75 // in kg 100 @Published var activityLevel: ActivityLevel = .lightlyActive 101 @Published var weightGoal: WeightGoal = .maintain 102 103 // Error tracking 104 @Published var lastError: String? 105 106 private enum CodingKeys: String, CodingKey { 107 case isOnboardingCompleted, age, gender, height, weight, targetWeight, activityLevel, weightGoal 108 } 109 110 init() { 111 load() 112 } 113 114 // Berechnung des Grundumsatzes nach Harris-Benedict-Formel 115 var basalMetabolicRate: Double { 116 if gender == .male { 117 // Männer: BMR = 66 + (13.7 × Gewicht in kg) + (5 × Größe in cm) - (6.8 × Alter in Jahren) 118 return 66 + (13.7 * weight) + (5 * height) - (6.8 * Double(age)) 119 } else { 120 // Frauen: BMR = 655 + (9.6 × Gewicht in kg) + (1.8 × Größe in cm) - (4.7 × Alter in Jahren) 121 return 655 + (9.6 * weight) + (1.8 * height) - (4.7 * Double(age)) 122 } 123 } 124 125 // Berechnung des täglichen Kalorienbedarfs 126 var dailyCalorieNeeds: Int { 127 let bmrWithActivity = basalMetabolicRate * activityLevel.factor 128 let adjustedCalories = bmrWithActivity * weightGoal.calorieAdjustment 129 return Int(adjustedCalories) 130 } 131 132 // Berechnung der empfohlenen Makronährstoffe 133 var recommendedMacros: (protein: Double, carbs: Double, fat: Double) { 134 let ratio = weightGoal.macroRatio 135 let caloriesFromProtein = Double(dailyCalorieNeeds) * ratio.protein 136 let caloriesFromCarbs = Double(dailyCalorieNeeds) * ratio.carbs 137 let caloriesFromFat = Double(dailyCalorieNeeds) * ratio.fat 138 139 // Umrechnung von Kalorien in Gramm: Protein 4 kcal/g, Kohlenhydrate 4 kcal/g, Fett 9 kcal/g 140 let protein = caloriesFromProtein / 4 141 let carbs = caloriesFromCarbs / 4 142 let fat = caloriesFromFat / 9 143 144 return (protein, carbs, fat) 145 } 146 147 // MARK: - Codable Konformität 148 required init(from decoder: Decoder) throws { 149 let container = try decoder.container(keyedBy: CodingKeys.self) 150 isOnboardingCompleted = try container.decode(Bool.self, forKey: .isOnboardingCompleted) 151 age = try container.decode(Int.self, forKey: .age) 152 gender = try container.decode(Gender.self, forKey: .gender) 153 height = try container.decode(Double.self, forKey: .height) 154 weight = try container.decode(Double.self, forKey: .weight) 155 targetWeight = try container.decode(Double.self, forKey: .targetWeight) 156 activityLevel = try container.decode(ActivityLevel.self, forKey: .activityLevel) 157 weightGoal = try container.decode(WeightGoal.self, forKey: .weightGoal) 158 } 159 160 func encode(to encoder: Encoder) throws { 161 var container = encoder.container(keyedBy: CodingKeys.self) 162 try container.encode(isOnboardingCompleted, forKey: .isOnboardingCompleted) 163 try container.encode(age, forKey: .age) 164 try container.encode(gender, forKey: .gender) 165 try container.encode(height, forKey: .height) 166 try container.encode(weight, forKey: .weight) 167 try container.encode(targetWeight, forKey: .targetWeight) 168 try container.encode(activityLevel, forKey: .activityLevel) 169 try container.encode(weightGoal, forKey: .weightGoal) 170 } 171 172 // MARK: - Speichern und Laden mit Versioning 173 private let saveKey = "UserProfileData" 174 175 func save() { 176 do { 177 let profileData = UserProfileData( 178 isOnboardingCompleted: isOnboardingCompleted, 179 age: age, 180 gender: gender, 181 height: height, 182 weight: weight, 183 targetWeight: targetWeight, 184 activityLevel: activityLevel, 185 weightGoal: weightGoal 186 ) 187 188 let persistedProfile = PersistedUserProfile(version: PersistedUserProfile.currentVersion, profile: profileData) 189 let encoded = try JSONEncoder().encode(persistedProfile) 190 UserDefaults.standard.set(encoded, forKey: saveKey) 191 lastError = nil 192 print("✅ UserProfile erfolgreich gespeichert (Version \(PersistedUserProfile.currentVersion))") 193 } catch { 194 lastError = "Fehler beim Speichern des Profils: \(error.localizedDescription)" 195 print("❌ Speicherfehler UserProfile: \(error)") 196 } 197 } 198 199 private func load() { 200 guard let savedData = UserDefaults.standard.data(forKey: saveKey) else { 201 print("ℹ️ Kein gespeichertes Profil gefunden - verwende Defaults") 202 return 203 } 204 205 do { 206 // Versuche zuerst, versioned data zu laden 207 let persistedProfile = try JSONDecoder().decode(PersistedUserProfile.self, from: savedData) 208 209 // Validiere und lade Profil 210 let profile = validateProfile(persistedProfile.profile) 211 applyProfile(profile) 212 213 print("✅ UserProfile geladen (Version \(persistedProfile.version))") 214 lastError = nil 215 216 } catch { 217 print("⚠️ Versioned load fehlgeschlagen, versuche legacy format...") 218 219 // Fallback: Versuche altes Format zu laden 220 do { 221 let legacyProfile = try JSONDecoder().decode(UserProfile.self, from: savedData) 222 223 // Kopiere Werte vom Legacy-Profil 224 self.isOnboardingCompleted = legacyProfile.isOnboardingCompleted 225 self.age = max(10, min(120, legacyProfile.age)) 226 self.gender = legacyProfile.gender 227 self.height = max(50, min(250, legacyProfile.height)) 228 self.weight = max(20, min(300, legacyProfile.weight)) 229 self.targetWeight = max(20, min(300, legacyProfile.targetWeight)) 230 self.activityLevel = legacyProfile.activityLevel 231 self.weightGoal = legacyProfile.weightGoal 232 233 // Speichere im neuen Format 234 save() 235 print("✅ Legacy UserProfile erfolgreich migriert") 236 237 } catch { 238 lastError = "Fehler beim Laden des Profils: \(error.localizedDescription)" 239 print("❌ Kritischer Fehler beim Laden von UserProfile: \(error)") 240 } 241 } 242 } 243 244 // Validierung des Profils 245 private func validateProfile(_ profile: UserProfileData) -> UserProfileData { 246 var validated = profile 247 248 // Validiere Wertebereiche 249 validated.age = max(10, min(120, profile.age)) 250 validated.height = max(50, min(250, profile.height)) 251 validated.weight = max(20, min(300, profile.weight)) 252 validated.targetWeight = max(20, min(300, profile.targetWeight)) 253 254 return validated 255 } 256 257 // Wende validierte Profil-Daten an 258 private func applyProfile(_ profile: UserProfileData) { 259 self.isOnboardingCompleted = profile.isOnboardingCompleted 260 self.age = profile.age 261 self.gender = profile.gender 262 self.height = profile.height 263 self.weight = profile.weight 264 self.targetWeight = profile.targetWeight 265 self.activityLevel = profile.activityLevel 266 self.weightGoal = profile.weightGoal 267 } 268 }