import Foundation import HealthKit /// Pilote la séance via HKLiveWorkoutBuilder et pousse les metrics vers l'iPhone. final class WorkoutManager: NSObject, ObservableObject { static let shared = WorkoutManager() @Published private(set) var heartRate = 0.0 @Published private(set) var activeEnergyKcal = 0.0 @Published private(set) var distanceMeters = 0.0 @Published private(set) var elapsedSec = 0.0 @Published private(set) var isRunning = false @Published private(set) var statusMessage: String? private let healthStore = HKHealthStore() private var session: HKWorkoutSession? private var builder: HKLiveWorkoutBuilder? private var distanceType = HKQuantityType(.distanceWalkingRunning) // Valeurs de travail, mutées uniquement dans les callbacks HealthKit (livrés sérialisés). private var bHeartRate = 0.0 private var bEnergy = 0.0 private var bDistance = 0.0 private var lastSentAt = Date.distantPast private let sendInterval: TimeInterval = 2 private override init() { super.init() } func startWorkout(activityType: HKWorkoutActivityType, locationType: HKWorkoutSessionLocationType) async { guard HKHealthStore.isHealthDataAvailable() else { setStatus("HealthKit indisponible sur cet appareil") return } distanceType = Self.distanceType(for: activityType) let hr = HKQuantityType(.heartRate) let energy = HKQuantityType(.activeEnergyBurned) let read: Set = [hr, energy, distanceType] let share: Set = [HKQuantityType.workoutType(), hr, energy, distanceType] do { try await healthStore.requestAuthorization(toShare: share, read: read) } catch { setStatus("Autorisation Santé refusée : \(error.localizedDescription)") return } let config = HKWorkoutConfiguration() config.activityType = activityType config.locationType = locationType do { let session = try HKWorkoutSession(healthStore: healthStore, configuration: config) let builder = session.associatedWorkoutBuilder() builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: config) session.delegate = self builder.delegate = self self.session = session self.builder = builder let start = Date() session.startActivity(with: start) try await builder.beginCollection(at: start) await MainActor.run { self.isRunning = true self.statusMessage = nil } } catch { setStatus("Démarrage séance impossible : \(error.localizedDescription)") } } func pause() { session?.pause() } func resume() { session?.resume() } func end() { session?.end() } private func setStatus(_ message: String) { DispatchQueue.main.async { [weak self] in self?.statusMessage = message } } private static func distanceType(for activity: HKWorkoutActivityType) -> HKQuantityType { activity == .cycling ? HKQuantityType(.distanceCycling) : HKQuantityType(.distanceWalkingRunning) } } extension WorkoutManager: HKLiveWorkoutBuilderDelegate { func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set) { let hrType = HKQuantityType(.heartRate) let energyType = HKQuantityType(.activeEnergyBurned) for type in collectedTypes { guard let qType = type as? HKQuantityType, let stats = workoutBuilder.statistics(for: qType) else { continue } if qType == hrType { let bpm = HKUnit.count().unitDivided(by: .minute()) if let v = stats.mostRecentQuantity()?.doubleValue(for: bpm) { bHeartRate = v } } else if qType == energyType { if let v = stats.sumQuantity()?.doubleValue(for: .kilocalorie()) { bEnergy = v } } else if qType == distanceType { if let v = stats.sumQuantity()?.doubleValue(for: .meter()) { bDistance = v } } } let elapsed = workoutBuilder.elapsedTime publish(elapsed: elapsed) maybeSend(elapsed: elapsed) } func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {} private func publish(elapsed: TimeInterval) { let hr = bHeartRate, kcal = bEnergy, dist = bDistance DispatchQueue.main.async { [weak self] in guard let self else { return } self.heartRate = hr self.activeEnergyKcal = kcal self.distanceMeters = dist self.elapsedSec = elapsed } } private func maybeSend(elapsed: TimeInterval) { let now = Date() guard now.timeIntervalSince(lastSentAt) >= sendInterval else { return } lastSentAt = now ConnectivityManager.shared.sendSample([ "heartRate": bHeartRate, "activeEnergyKcal": bEnergy, "distanceMeters": bDistance, "elapsedSec": elapsed, "ts": now.timeIntervalSince1970 ]) } } extension WorkoutManager: HKWorkoutSessionDelegate { func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { guard toState == .ended, let builder else { return } Task { [weak self] in try? await builder.endCollection(at: date) _ = try? await builder.finishWorkout() await MainActor.run { self?.isRunning = false } } } func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { setStatus("Erreur séance : \(error.localizedDescription)") DispatchQueue.main.async { [weak self] in self?.isRunning = false } } }