/ BalanceKit / WorkoutTracker.swift
WorkoutTracker.swift
1 // 2 // WorkoutTracker.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 PersistedWorkoutData: Codable { 12 let version: Int 13 let workouts: [Workout] 14 15 static let currentVersion = 1 16 } 17 18 // MARK: - Workout Types 19 enum WorkoutType: String, CaseIterable, Identifiable, Codable { 20 case walking = "Gehen" 21 case running = "Laufen" 22 case cycling = "Radfahren" 23 case swimming = "Schwimmen" 24 case weightLifting = "Krafttraining" 25 case yoga = "Yoga" 26 case hiit = "HIIT-Training" 27 case other = "Sonstiges" 28 29 var id: String { self.rawValue } 30 31 // Durchschnittlicher Kalorienverbrauch pro Stunde für eine 70kg Person 32 // Tatsächlicher Verbrauch variiert je nach Gewicht, Alter, Geschlecht, Intensität 33 var caloriesPerHourBase: Int { 34 switch self { 35 case .walking: return 280 36 case .running: return 600 37 case .cycling: return 450 38 case .swimming: return 500 39 case .weightLifting: return 350 40 case .yoga: return 250 41 case .hiit: return 700 42 case .other: return 400 43 } 44 } 45 46 // Icon für die Anzeige 47 var icon: String { 48 switch self { 49 case .walking: return "figure.walk" 50 case .running: return "figure.run" 51 case .cycling: return "bicycle" 52 case .swimming: return "figure.pool.swim" 53 case .weightLifting: return "dumbbell" 54 case .yoga: return "figure.mind.and.body" 55 case .hiit: return "figure.highintensity.intervaltraining" 56 case .other: return "figure.mixed.cardio" 57 } 58 } 59 } 60 61 // Struktur für eine Trainingseinheit 62 struct Workout: Identifiable, Codable { 63 var id = UUID() 64 var type: WorkoutType 65 var duration: TimeInterval // in Sekunden 66 var date: Date 67 var caloriesBurned: Int 68 var notes: String = "" 69 70 // Berechnung der verbrannten Kalorien basierend auf Aktivität, Dauer und Gewicht 71 static func calculateCalories(type: WorkoutType, durationInMinutes: Double, weightInKg: Double) -> Int { 72 let durationInHours = durationInMinutes / 60.0 73 74 // Gewichtsanpassungsfaktor (Referenzgewicht ist 70kg) 75 let weightFactor = weightInKg / 70.0 76 77 // Berechne Kalorien basierend auf Aktivität, Dauer und angepasst an das Gewicht 78 let baseCalories = Double(type.caloriesPerHourBase) * durationInHours 79 let adjustedCalories = baseCalories * weightFactor 80 81 return Int(adjustedCalories) 82 } 83 } 84 85 // Manager für Trainingseinheiten 86 class WorkoutManager: ObservableObject { 87 @Published var workouts: [Workout] = [] { 88 didSet { 89 save() 90 } 91 } 92 93 // Error tracking 94 @Published var lastError: String? 95 96 private let saveKey = "SavedWorkouts" 97 98 init() { 99 load() 100 } 101 102 // Neue Trainingseinheit hinzufügen 103 func addWorkout(type: WorkoutType, durationInMinutes: Double, date: Date, userWeight: Double, notes: String = "") { 104 let caloriesBurned = Workout.calculateCalories(type: type, durationInMinutes: durationInMinutes, weightInKg: userWeight) 105 106 let newWorkout = Workout( 107 type: type, 108 duration: durationInMinutes * 60, // Umrechnung in Sekunden 109 date: date, 110 caloriesBurned: caloriesBurned, 111 notes: notes 112 ) 113 114 workouts.append(newWorkout) 115 } 116 117 // Trainingseinheit löschen 118 func deleteWorkout(at indexSet: IndexSet) { 119 workouts.remove(atOffsets: indexSet) 120 } 121 122 // Trainingseinheiten für einen bestimmten Tag 123 func workoutsForDay(_ date: Date) -> [Workout] { 124 let calendar = Calendar.current 125 let startOfDay = calendar.startOfDay(for: date) 126 guard let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) else { 127 return [] 128 } 129 130 return workouts.filter { $0.date >= startOfDay && $0.date < endOfDay } 131 } 132 133 // Gesamtzahl der verbrannten Kalorien für einen bestimmten Tag (nur manuell eingegebene Workouts) 134 func totalCaloriesBurnedForDay(_ date: Date) -> Int { 135 let dayWorkouts = workoutsForDay(date) 136 return dayWorkouts.reduce(0) { $0 + $1.caloriesBurned } 137 } 138 139 // Gesamtzahl der verbrannten Kalorien für einen bestimmten Tag inkl. HealthKit-Daten 140 func totalCaloriesBurnedWithHealthKit(date: Date, healthManager: HealthManager?) -> Int { 141 let workoutCalories = totalCaloriesBurnedForDay(date) 142 143 guard let healthManager = healthManager else { 144 return workoutCalories 145 } 146 147 // Für heute können wir direkt den aktuellen Wert verwenden 148 let isToday = Calendar.current.isDateInToday(date) 149 150 if isToday { 151 let total = workoutCalories + Int(healthManager.activeCaloriesBurned) 152 print("DEBUG WorkoutTracker (heute): workouts=\(workoutCalories), healthKit=\(Int(healthManager.activeCaloriesBurned)), total=\(total)") 153 return total 154 } else { 155 // Für andere Tage versuchen wir gecachte Daten zu verwenden 156 if let cachedData = healthManager.getCachedHealthData(for: date) { 157 let total = workoutCalories + Int(cachedData.activeCalories) 158 print("DEBUG WorkoutTracker (cache): workouts=\(workoutCalories), healthKit=\(Int(cachedData.activeCalories)), total=\(total)") 159 return total 160 } else { 161 // Wenn keine gecachten Daten vorhanden sind, versuche sie zu laden 162 healthManager.fetchActiveCaloriesForDate(date) { calories in 163 print("Aktive Kalorien für \(date): \(calories)") 164 } 165 print("DEBUG WorkoutTracker (no cache): workouts=\(workoutCalories), healthKit=0, total=\(workoutCalories)") 166 return workoutCalories 167 } 168 } 169 } 170 171 // Gesamtzahl der verbrannten Kalorien für eine bestimmte Woche 172 func totalCaloriesBurnedForWeek(startingFrom date: Date) -> Int { 173 let calendar = Calendar.current 174 let startOfDay = calendar.startOfDay(for: date) 175 guard let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay) else { 176 return 0 177 } 178 179 let weekWorkouts = workouts.filter { $0.date >= startOfDay && $0.date < endDate } 180 return weekWorkouts.reduce(0) { $0 + $1.caloriesBurned } 181 } 182 183 // Speichern der Daten in UserDefaults mit Versioning 184 private func save() { 185 do { 186 let persistedData = PersistedWorkoutData(version: PersistedWorkoutData.currentVersion, workouts: workouts) 187 let encoded = try JSONEncoder().encode(persistedData) 188 UserDefaults.standard.set(encoded, forKey: saveKey) 189 lastError = nil 190 print("✅ Workouts erfolgreich gespeichert (Version \(PersistedWorkoutData.currentVersion), \(workouts.count) Workouts)") 191 } catch { 192 lastError = "Fehler beim Speichern der Workouts: \(error.localizedDescription)" 193 print("❌ Speicherfehler Workouts: \(error)") 194 } 195 } 196 197 // Laden der Daten aus UserDefaults mit Migration 198 private func load() { 199 guard let savedWorkouts = UserDefaults.standard.data(forKey: saveKey) else { 200 print("ℹ️ Keine gespeicherten Workouts gefunden") 201 workouts = [] 202 return 203 } 204 205 do { 206 // Versuche zuerst, versioned data zu laden 207 let persistedData = try JSONDecoder().decode(PersistedWorkoutData.self, from: savedWorkouts) 208 209 // Validiere und lade Workouts 210 workouts = validateAndCleanWorkouts(persistedData.workouts) 211 print("✅ Workouts geladen (Version \(persistedData.version), \(workouts.count) Workouts)") 212 lastError = nil 213 214 } catch { 215 print("⚠️ Versioned load fehlgeschlagen, versuche legacy format...") 216 217 // Fallback: Versuche altes Format zu laden 218 do { 219 let legacyWorkouts = try JSONDecoder().decode([Workout].self, from: savedWorkouts) 220 221 // Validiere und migriere Legacy-Daten 222 workouts = validateAndCleanWorkouts(legacyWorkouts) 223 224 // Speichere sofort im neuen Format 225 save() 226 print("✅ Legacy Workouts erfolgreich migriert (\(workouts.count) Workouts)") 227 228 } catch { 229 lastError = "Fehler beim Laden der Workouts: \(error.localizedDescription)" 230 print("❌ Kritischer Fehler beim Laden von Workouts: \(error)") 231 workouts = [] 232 } 233 } 234 } 235 236 // Validierung und Bereinigung von Workouts 237 private func validateAndCleanWorkouts(_ workouts: [Workout]) -> [Workout] { 238 return workouts.filter { workout in 239 // Validierungskriterien 240 let isValid = workout.duration > 0 && 241 workout.caloriesBurned >= 0 && 242 workout.date <= Date() 243 244 if !isValid { 245 print("⚠️ Invalides Workout gefiltert: \(workout.type.rawValue)") 246 } 247 248 return isValid 249 } 250 } 251 }