155 lines
4.1 KiB
Swift
155 lines
4.1 KiB
Swift
import Combine
|
|
import Foundation
|
|
import HealthKit
|
|
|
|
@MainActor
|
|
class HealthKitService: ObservableObject {
|
|
static let shared = HealthKitService()
|
|
|
|
private let healthStore = HKHealthStore()
|
|
private var currentWorkoutStartDate: Date?
|
|
private var currentWorkoutSessionId: UUID?
|
|
|
|
@Published var isAuthorized = false
|
|
@Published var isEnabled = false
|
|
|
|
private let userDefaults = UserDefaults.standard
|
|
private let isEnabledKey = "healthKitEnabled"
|
|
|
|
private init() {
|
|
loadSettings()
|
|
}
|
|
|
|
func loadSettings() {
|
|
isEnabled = userDefaults.bool(forKey: isEnabledKey)
|
|
|
|
if HKHealthStore.isHealthDataAvailable() {
|
|
checkAuthorization()
|
|
}
|
|
}
|
|
|
|
func setEnabled(_ enabled: Bool) {
|
|
isEnabled = enabled
|
|
userDefaults.set(enabled, forKey: isEnabledKey)
|
|
}
|
|
|
|
func requestAuthorization() async throws {
|
|
guard HKHealthStore.isHealthDataAvailable() else {
|
|
throw HealthKitError.notAvailable
|
|
}
|
|
|
|
let workoutType = HKObjectType.workoutType()
|
|
let energyBurnedType = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!
|
|
|
|
let typesToShare: Set<HKSampleType> = [
|
|
workoutType,
|
|
energyBurnedType,
|
|
]
|
|
|
|
let typesToRead: Set<HKObjectType> = [
|
|
workoutType
|
|
]
|
|
|
|
try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead)
|
|
|
|
self.isAuthorized = true
|
|
}
|
|
|
|
private func checkAuthorization() {
|
|
let workoutType = HKObjectType.workoutType()
|
|
let status = healthStore.authorizationStatus(for: workoutType)
|
|
|
|
isAuthorized = (status == .sharingAuthorized)
|
|
}
|
|
|
|
func startWorkout(startDate: Date, sessionId: UUID) async throws {
|
|
guard isEnabled && isAuthorized else {
|
|
return
|
|
}
|
|
|
|
guard HKHealthStore.isHealthDataAvailable() else {
|
|
throw HealthKitError.notAvailable
|
|
}
|
|
|
|
currentWorkoutStartDate = startDate
|
|
currentWorkoutSessionId = sessionId
|
|
}
|
|
|
|
func endWorkout(endDate: Date) async throws {
|
|
guard isEnabled && isAuthorized else {
|
|
return
|
|
}
|
|
|
|
guard let startDate = currentWorkoutStartDate else {
|
|
return
|
|
}
|
|
|
|
guard HKHealthStore.isHealthDataAvailable() else {
|
|
throw HealthKitError.notAvailable
|
|
}
|
|
|
|
let duration = endDate.timeIntervalSince(startDate)
|
|
let calories = estimateCalories(durationInMinutes: duration / 60.0)
|
|
|
|
let energyBurned = HKQuantity(unit: .kilocalorie(), doubleValue: calories)
|
|
|
|
let workout = HKWorkout(
|
|
activityType: .climbing,
|
|
start: startDate,
|
|
end: endDate,
|
|
duration: duration,
|
|
totalEnergyBurned: energyBurned,
|
|
totalDistance: nil,
|
|
metadata: [
|
|
HKMetadataKeyIndoorWorkout: true
|
|
]
|
|
)
|
|
|
|
do {
|
|
try await healthStore.save(workout)
|
|
|
|
currentWorkoutStartDate = nil
|
|
currentWorkoutSessionId = nil
|
|
} catch {
|
|
currentWorkoutStartDate = nil
|
|
currentWorkoutSessionId = nil
|
|
|
|
throw HealthKitError.workoutSaveFailed
|
|
}
|
|
}
|
|
|
|
func cancelWorkout() {
|
|
currentWorkoutStartDate = nil
|
|
currentWorkoutSessionId = nil
|
|
}
|
|
|
|
func hasActiveWorkout() -> Bool {
|
|
return currentWorkoutStartDate != nil
|
|
}
|
|
|
|
private func estimateCalories(durationInMinutes: Double) -> Double {
|
|
let caloriesPerMinute = 8.0
|
|
return durationInMinutes * caloriesPerMinute
|
|
}
|
|
}
|
|
|
|
enum HealthKitError: LocalizedError {
|
|
case notAvailable
|
|
case notAuthorized
|
|
case workoutStartFailed
|
|
case workoutSaveFailed
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .notAvailable:
|
|
return "HealthKit is not available on this device"
|
|
case .notAuthorized:
|
|
return "HealthKit authorization not granted"
|
|
case .workoutStartFailed:
|
|
return "Failed to start HealthKit workout"
|
|
case .workoutSaveFailed:
|
|
return "Failed to save workout to HealthKit"
|
|
}
|
|
}
|
|
}
|