/ BalanceKit / HealthManager.swift
HealthManager.swift
  1  import Foundation
  2  import HealthKit
  3  import OSLog
  4  
  5  // MARK: - Cache Structure
  6  struct HealthDataCache: Codable {
  7      var activeCalories: Double
  8      var steps: Int
  9      var waterConsumed: Double
 10      var lastUpdate: Date
 11      var cacheDate: Date // Für welches Datum gelten diese Daten
 12      
 13      func isValid(for date: Date, maxAge: TimeInterval = 300) -> Bool {
 14          let calendar = Calendar.current
 15          let isSameDay = calendar.isDate(cacheDate, inSameDayAs: date)
 16          let cacheAge = Date().timeIntervalSince(lastUpdate)
 17          return isSameDay && cacheAge < maxAge
 18      }
 19  }
 20  
 21  class HealthManager: ObservableObject {
 22      let healthStore = HKHealthStore()
 23      @Published var activeCaloriesBurned: Double = 0
 24      @Published var steps: Int = 0
 25      @Published var waterConsumed: Double = 0 // Wasser in Millilitern (ml)
 26      @Published var isAuthorized: Bool = false
 27      
 28      // Cache Management
 29      private var cachedData: [String: HealthDataCache] = [:]
 30      private let cacheKey = "HealthKitDataCache"
 31      private let cacheMaxAge: TimeInterval = 300 // 5 Minuten
 32      
 33      init() {
 34          loadCache()
 35          
 36          // Im Simulator auch ohne HealthKit-Verfügbarkeit initialisieren
 37          #if targetEnvironment(simulator)
 38          #if DEBUG
 39          Logger.healthKit.debug("Simulator-Modus aktiviert, verwende Mock-Daten")
 40          #endif
 41          // Im Simulator verwenden wir Standardwerte
 42          isAuthorized = true
 43          waterConsumed = 0.0
 44          #else
 45          if HKHealthStore.isHealthDataAvailable() {
 46              requestAuthorization()
 47          }
 48          #endif
 49      }
 50      
 51      func requestAuthorization() {
 52          // Definieren welche Datentypen wir lesen wollen
 53          let typesToRead: Set<HKObjectType> = [
 54              HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
 55              HKObjectType.quantityType(forIdentifier: .stepCount)!,
 56              HKObjectType.quantityType(forIdentifier: .dietaryWater)! // Wasser-Tracking hinzugefügt
 57          ]
 58          
 59          // Definieren welche Datentypen wir schreiben wollen
 60          let typesToWrite: Set<HKSampleType> = [
 61              HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)!,
 62              HKObjectType.quantityType(forIdentifier: .dietaryProtein)!,
 63              HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)!,
 64              HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)!,
 65              HKObjectType.quantityType(forIdentifier: .dietaryWater)! // Wasser-Tracking hinzugefügt
 66          ]
 67          
 68          // Zugriff anfragen
 69          healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { success, error in
 70              DispatchQueue.main.async {
 71                  self.isAuthorized = success
 72                  if success {
 73                      Logger.healthKit.info("HealthKit Zugriff gewährt")
 74                      self.fetchTodaysData()
 75                  } else if let error = error {
 76                      Logger.healthKit.error("HealthKit Zugriff verweigert: \(error.localizedDescription)")
 77                  }
 78              }
 79          }
 80      }
 81      
 82      func fetchTodaysData() {
 83          let today = Date()
 84          
 85          // Prüfe Cache zuerst
 86          if let cache = getCachedData(for: today), cache.isValid(for: today, maxAge: cacheMaxAge) {
 87              #if DEBUG
 88              Logger.healthKit.debug("HealthKit Daten aus Cache geladen (Alter: \(Int(Date().timeIntervalSince(cache.lastUpdate)))s)")
 89              #endif
 90              DispatchQueue.main.async {
 91                  self.activeCaloriesBurned = cache.activeCalories
 92                  self.steps = cache.steps
 93                  self.waterConsumed = cache.waterConsumed
 94              }
 95              return
 96          }
 97          
 98          // Andernfalls von HealthKit laden
 99          #if DEBUG
100          Logger.healthKit.debug("Lade frische HealthKit Daten...")
101          #endif
102          fetchSteps()
103          fetchActiveCalories()
104          fetchWaterIntake()
105      }
106      
107      // Generische Helper-Methode für HealthKit Abfragen
108      private func fetchQuantityForDate(_ date: Date, typeIdentifier: HKQuantityTypeIdentifier, unit: HKUnit, completion: @escaping (Double) -> Void) {
109          #if targetEnvironment(simulator)
110          completion(0)
111          return
112          #else
113          guard let quantityType = HKQuantityType.quantityType(forIdentifier: typeIdentifier) else {
114              completion(0)
115              return
116          }
117          
118          let calendar = Calendar.current
119          let startOfDay = calendar.startOfDay(for: date)
120          guard let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) else {
121              Logger.healthKit.error("Fehler beim Berechnen des Tagesendes für \(date)")
122              completion(0)
123              return
124          }
125          
126          let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: endOfDay, options: .strictStartDate)
127          
128          let query = HKStatisticsQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
129              guard let result = result, let sum = result.sumQuantity() else {
130                  if let error = error {
131                      Logger.healthKit.error("Fehler beim Laden von \(typeIdentifier.rawValue) für \(date): \(error.localizedDescription)")
132                  }
133                  completion(0)
134                  return
135              }
136              
137              let value = sum.doubleValue(for: unit)
138              completion(value)
139          }
140          
141          healthStore.execute(query)
142          #endif
143      }
144      
145      // Schritte für ein bestimmtes Datum abrufen
146      func fetchStepsForDate(_ date: Date, completion: @escaping (Int) -> Void) {
147          fetchQuantityForDate(date, typeIdentifier: .stepCount, unit: HKUnit.count()) { value in
148              completion(Int(value))
149          }
150      }
151      
152      // Aktive Kalorien für ein bestimmtes Datum abrufen und cachen
153      func fetchActiveCaloriesForDate(_ date: Date, completion: @escaping (Double) -> Void) {
154          fetchQuantityForDate(date, typeIdentifier: .activeEnergyBurned, unit: HKUnit.kilocalorie()) { calories in
155              // Cache die Daten, damit sie beim nächsten Zugriff verfügbar sind
156              DispatchQueue.main.async {
157                  let cacheData = HealthDataCache(
158                      activeCalories: calories,
159                      steps: 0, // Wird später aktualisiert, falls benötigt
160                      waterConsumed: 0, // Wird später aktualisiert, falls benötigt
161                      lastUpdate: Date(),
162                      cacheDate: date
163                  )
164                  self.updateCache(date: date, data: cacheData)
165                  #if DEBUG
166                  Logger.healthKit.debug("HealthKit Cache für \(date) aktualisiert: \(calories) kcal")
167                  #endif
168                  completion(calories)
169              }
170          }
171      }
172      
173      // Schritte abrufen (für heute)
174      func fetchSteps() {
175          let stepsType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
176          let now = Date()
177          let startOfDay = Calendar.current.startOfDay(for: now)
178          
179          let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
180          
181          let query = HKStatisticsQuery(quantityType: stepsType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
182              guard let result = result, let sum = result.sumQuantity() else {
183                  Logger.healthKit.error("Fehler beim Laden der Schritte: \(error?.localizedDescription ?? "Unbekannter Fehler")")
184                  return
185              }
186              
187              let steps = sum.doubleValue(for: HKUnit.count())
188              
189              DispatchQueue.main.async {
190                  self.steps = Int(steps)
191                  self.updateCache(date: now)
192              }
193          }
194          
195          healthStore.execute(query)
196      }
197      
198      // Aktive Kalorien abrufen (für heute)
199      func fetchActiveCalories() {
200          let caloriesType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
201          let now = Date()
202          let startOfDay = Calendar.current.startOfDay(for: now)
203          
204          let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
205          
206          let query = HKStatisticsQuery(quantityType: caloriesType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
207              guard let result = result, let sum = result.sumQuantity() else {
208                  Logger.healthKit.error("Fehler beim Laden der aktiven Kalorien: \(error?.localizedDescription ?? "Unbekannter Fehler")")
209                  return
210              }
211              
212              let calories = sum.doubleValue(for: HKUnit.kilocalorie())
213              
214              DispatchQueue.main.async {
215                  self.activeCaloriesBurned = calories
216                  self.updateCache(date: now)
217              }
218          }
219          
220          healthStore.execute(query)
221      }
222      
223      // Nahrungsinformationen in Health App speichern
224      func saveFoodItem(name: String, calories: Double, protein: Double, carbs: Double, fat: Double) {
225          guard isAuthorized else {
226              Logger.healthKit.warning("HealthKit nicht autorisiert")
227              return
228          }
229          
230          // Metadata für den Eintrag
231          let metadata: [String: Any] = [
232              HKMetadataKeyFoodType: name
233          ]
234          
235          // Kalorien speichern
236          let caloriesType = HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
237          let caloriesQuantity = HKQuantity(unit: HKUnit.kilocalorie(), doubleValue: calories)
238          let caloriesSample = HKQuantitySample(type: caloriesType, quantity: caloriesQuantity, start: Date(), end: Date(), metadata: metadata)
239          
240          // Protein speichern
241          let proteinType = HKQuantityType.quantityType(forIdentifier: .dietaryProtein)!
242          let proteinQuantity = HKQuantity(unit: HKUnit.gram(), doubleValue: protein)
243          let proteinSample = HKQuantitySample(type: proteinType, quantity: proteinQuantity, start: Date(), end: Date(), metadata: metadata)
244          
245          // Kohlenhydrate speichern
246          let carbsType = HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates)!
247          let carbsQuantity = HKQuantity(unit: HKUnit.gram(), doubleValue: carbs)
248          let carbsSample = HKQuantitySample(type: carbsType, quantity: carbsQuantity, start: Date(), end: Date(), metadata: metadata)
249          
250          // Fett speichern
251          let fatType = HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal)!
252          let fatQuantity = HKQuantity(unit: HKUnit.gram(), doubleValue: fat)
253          let fatSample = HKQuantitySample(type: fatType, quantity: fatQuantity, start: Date(), end: Date(), metadata: metadata)
254          
255          // Alle Daten speichern
256          healthStore.save([caloriesSample, proteinSample, carbsSample, fatSample]) { success, error in
257              if let error = error {
258                  Logger.healthKit.error("Fehler beim Speichern in Health: \(error.localizedDescription)")
259              } else {
260                  Logger.healthKit.info("Erfolgreich in Health gespeichert: \(name)")
261              }
262          }
263      }
264      
265      // Wasserkonsum für ein bestimmtes Datum abrufen
266      func fetchWaterIntakeForDate(_ date: Date, completion: @escaping (Double) -> Void) {
267          #if DEBUG
268          Logger.healthKit.debug("Wasser-Tracking - Lade Wasserkonsum für Datum: \(date)")
269          #endif
270          fetchQuantityForDate(date, typeIdentifier: .dietaryWater, unit: HKUnit.liter()) { liters in
271              let milliliters = liters * 1000
272              #if DEBUG
273              Logger.healthKit.debug("Wasser-Tracking - Geladen für \(date): \(milliliters) ml")
274              #endif
275              completion(milliliters)
276          }
277      }
278      
279      // Wasserkonsum aus HealthKit abrufen (für heute)
280      func fetchWaterIntake() {
281          #if DEBUG
282          Logger.healthKit.debug("Wasser-Tracking - Lade Wasserkonsum")
283          #endif
284          
285          #if targetEnvironment(simulator)
286          // Im Simulator ist waterConsumed bereits initialisiert (siehe init)
287          // Wir müssen hier nichts tun, da der Wert bereits gesetzt ist
288          #if DEBUG
289          Logger.healthKit.debug("Wasser-Tracking - Simulator-Modus, verwende vorhandenen Wert: \(self.waterConsumed)")
290          #endif
291          #else
292          let waterType = HKQuantityType.quantityType(forIdentifier: .dietaryWater)!
293          let now = Date()
294          let startOfDay = Calendar.current.startOfDay(for: now)
295          
296          let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
297          
298          let query = HKStatisticsQuery(quantityType: waterType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
299              if let error = error {
300                  Logger.healthKit.error("Wasser-Tracking - Fehler beim Laden: \(error.localizedDescription)")
301                  return
302              }
303              
304              if let result = result, let sum = result.sumQuantity() {
305                  let waterInLiters = sum.doubleValue(for: HKUnit.liter())
306                  let waterInML = waterInLiters * 1000  // Konvertierung in Milliliter
307                  
308                  #if DEBUG
309                  Logger.healthKit.debug("Wasser-Tracking - Geladen: \(waterInML) ml")
310                  #endif
311                  
312                  DispatchQueue.main.async {
313                      self.waterConsumed = waterInML
314                      self.updateCache(date: now)
315                  }
316              } else {
317                  #if DEBUG
318                  Logger.healthKit.debug("Wasser-Tracking - Keine Daten gefunden")
319                  #endif
320                  // Setze Wert auf 0, um die View zu aktualisieren
321                  DispatchQueue.main.async {
322                      self.waterConsumed = 0
323                      self.updateCache(date: now)
324                  }
325              }
326          }
327          
328          healthStore.execute(query)
329          #endif
330      }
331      
332      // Wasser zur Health App hinzufügen
333      func addWater(milliliters: Double) {
334          #if DEBUG
335          Logger.healthKit.debug("Wasser-Tracking - Hinzufügen von \(milliliters) ml")
336          #endif
337          
338          #if targetEnvironment(simulator)
339          // Im Simulator direkt den Wert erhöhen ohne HealthKit
340          #if DEBUG
341          Logger.healthKit.debug("Wasser-Tracking - Simulator-Modus, direkte Aktualisierung")
342          #endif
343          DispatchQueue.main.async {
344              self.waterConsumed += milliliters
345              self.objectWillChange.send()
346              self.updateCache(date: Date())
347          }
348          #else
349          guard isAuthorized else {
350              Logger.healthKit.warning("Wasser-Tracking - HealthKit nicht autorisiert")
351              return
352          }
353          
354          // Konvertiere Milliliter in Liter für HealthKit
355          let liters = milliliters / 1000
356          
357          // Sofortige UI-Aktualisierung für besseres Nutzererlebnis
358          DispatchQueue.main.async {
359              self.waterConsumed += milliliters
360              self.objectWillChange.send()
361              self.updateCache(date: Date())
362          }
363          
364          // Metadata für den Eintrag
365          let metadata: [String: Any] = [
366              HKMetadataKeyWasUserEntered: true
367          ]
368          
369          // Wasser speichern
370          let waterType = HKQuantityType.quantityType(forIdentifier: .dietaryWater)!
371          let waterQuantity = HKQuantity(unit: HKUnit.liter(), doubleValue: liters)
372          let waterSample = HKQuantitySample(type: waterType, quantity: waterQuantity, start: Date(), end: Date(), metadata: metadata)
373          
374          // Speichern des Wassereintrags
375          healthStore.save(waterSample) { success, error in
376              if let error = error {
377                  Logger.healthKit.error("Wasser-Tracking - Fehler beim Speichern: \(error.localizedDescription)")
378                  // Bei Fehler den Wert zurücksetzen
379                  DispatchQueue.main.async {
380                      self.waterConsumed -= milliliters
381                      self.objectWillChange.send()
382                      self.updateCache(date: Date())
383                  }
384              } else {
385                  #if DEBUG
386                  Logger.healthKit.debug("Wasser-Tracking - Erfolgreich in HealthKit gespeichert: \(milliliters) ml")
387                  #endif
388              }
389          }
390          #endif
391      }
392      
393      // MARK: - Cache Management
394      private func getCacheKey(for date: Date) -> String {
395          let formatter = DateFormatter()
396          formatter.dateFormat = "yyyy-MM-dd"
397          return formatter.string(from: date)
398      }
399      
400      private func getCachedData(for date: Date) -> HealthDataCache? {
401          let key = getCacheKey(for: date)
402          return cachedData[key]
403      }
404      
405      private func updateCache(date: Date) {
406          let key = getCacheKey(for: date)
407          let cache = HealthDataCache(
408              activeCalories: activeCaloriesBurned,
409              steps: steps,
410              waterConsumed: waterConsumed,
411              lastUpdate: Date(),
412              cacheDate: date
413          )
414          cachedData[key] = cache
415          saveCache()
416      }
417      
418      // Überladene Version zum direkten Setzen von Cache-Daten
419      private func updateCache(date: Date, data: HealthDataCache) {
420          let key = getCacheKey(for: date)
421          cachedData[key] = data
422          saveCache()
423          // Trigger UI update
424          objectWillChange.send()
425      }
426      
427      private func saveCache() {
428          do {
429              let encoder = JSONEncoder()
430              let data = try encoder.encode(cachedData)
431              UserDefaults.standard.set(data, forKey: cacheKey)
432              #if DEBUG
433              Logger.healthKit.debug("HealthKit Cache gespeichert (\(self.cachedData.count) Tage)")
434              #endif
435          } catch {
436              Logger.healthKit.error("❌ Fehler beim Speichern des HealthKit Cache: \(error)")
437          }
438      }
439      
440      private func loadCache() {
441          guard let data = UserDefaults.standard.data(forKey: cacheKey) else {
442              #if DEBUG
443              Logger.healthKit.info("ℹ️ Kein HealthKit Cache gefunden")
444              #endif
445              return
446          }
447          
448          do {
449              let decoder = JSONDecoder()
450              cachedData = try decoder.decode([String: HealthDataCache].self, from: data)
451              
452              // Bereinige alten Cache (älter als 7 Tage)
453              guard let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) else {
454                  Logger.healthKit.error("Fehler beim Berechnen des Cache-Bereinigungsdatums")
455                  return
456              }
457              cachedData = cachedData.filter { _, cache in
458                  cache.cacheDate >= sevenDaysAgo
459              }
460              
461              #if DEBUG
462              Logger.healthKit.debug("✅ HealthKit Cache geladen (\(self.cachedData.count) Tage)")
463              #endif
464          } catch {
465              Logger.healthKit.error("❌ Fehler beim Laden des HealthKit Cache: \(error)")
466              cachedData = [:]
467          }
468      }
469      
470      // Öffentliche Funktion zum Abrufen gecachter Daten für historische Ansichten
471      func getCachedHealthData(for date: Date) -> (activeCalories: Double, steps: Int, waterConsumed: Double)? {
472          if let cache = getCachedData(for: date), cache.isValid(for: date, maxAge: .infinity) {
473              return (cache.activeCalories, cache.steps, cache.waterConsumed)
474          }
475          return nil
476      }
477  }