// // HealthKitManager.swift // digitalpilates // // Updated module for HealthKit authorization and sleep data access. // import Foundation import React import HealthKit @objc(HealthKitManager) class HealthKitManager: NSObject, RCTBridgeModule { private let healthStore = HKHealthStore() static func moduleName() -> String! { return "HealthKitManager" } static func requiresMainQueueSetup() -> Bool { return true } // MARK: - Types We Care About /// For reading private struct ReadTypes { static let sleep = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)! static let stepCount = HKObjectType.quantityType(forIdentifier: .stepCount)! static let heartRate = HKObjectType.quantityType(forIdentifier: .heartRate)! static let heartRateVariability = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)! static let activeEnergyBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)! static let basalEnergyBurned = HKObjectType.quantityType(forIdentifier: .basalEnergyBurned)! static let appleExerciseTime = HKObjectType.quantityType(forIdentifier: .appleExerciseTime)! static let appleStandTime = HKObjectType.categoryType(forIdentifier: .appleStandHour)! static let oxygenSaturation = HKObjectType.quantityType(forIdentifier: .oxygenSaturation)! static let activitySummary = HKObjectType.activitySummaryType() static let dietaryWater = HKObjectType.quantityType(forIdentifier: .dietaryWater)! static var all: Set { return [sleep, stepCount, heartRate, heartRateVariability, activeEnergyBurned, basalEnergyBurned, appleExerciseTime, appleStandTime, oxygenSaturation, activitySummary, dietaryWater] } } /// For writing (if needed) private struct WriteTypes { static let bodyMass = HKObjectType.quantityType(forIdentifier: .bodyMass)! static let dietaryWater = HKObjectType.quantityType(forIdentifier: .dietaryWater)! static var all: Set { return [bodyMass, dietaryWater] } } // MARK: - Authorization @objc func requestAuthorization( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } healthStore.requestAuthorization(toShare: WriteTypes.all, read: ReadTypes.all) { [weak self] (success, error) in DispatchQueue.main.async { if let error = error { rejecter("AUTHORIZATION_ERROR", "Failed to authorize HealthKit: \(error.localizedDescription)", error) return } if success { // We don't rely purely on authorizationStatus(for:) for quantity types, see below. var permissions: [String: Any] = [:] for type in ReadTypes.all { if type == ReadTypes.sleep { // For categoryType, authorizationStatus is meaningful let status = self?.healthStore.authorizationStatus(for: type) ?? .notDetermined permissions[type.identifier] = self?.authorizationStatusToString(status) } else { // For quantity types, authorizationStatus isn't reliable – mark as “unknownUntilQueried” permissions[type.identifier] = "unknownUntilQueried" } } // Return success immediately let result: [String: Any] = [ "success": true, "permissions": permissions ] resolver(result) } else { rejecter("AUTHORIZATION_DENIED", "User denied HealthKit authorization", nil) } } } } // MARK: - Current Authorization Status Check @objc func getAuthorizationStatus( _ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } var permissions: [String: Any] = [:] for type in ReadTypes.all { if type == ReadTypes.sleep { let status = healthStore.authorizationStatus(for: type) permissions[type.identifier] = authorizationStatusToString(status) } else { // For quantity types, we cannot rely on authorizationStatus; attempt a small read test (optional) // But to keep simple, we mark as “unknownUntilQueried” permissions[type.identifier] = "unknownUntilQueried" } } let result: [String: Any] = [ "success": true, "permissions": permissions ] resolver(result) } // MARK: - Sleep Data @objc func getSleepData( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)! // Parse options let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())! } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query sleep data: \(error.localizedDescription)", error) return } guard let sleepSamples = samples as? [HKCategorySample] else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let sleepData = sleepSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.value, "categoryType": self?.sleepValueToString(sample.value) ?? "unknown", "duration": sample.endDate.timeIntervalSince(sample.startDate), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let result: [String: Any] = [ "data": sleepData, "count": sleepData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } // MARK: - Fitness Data Methods @objc func getActiveEnergyBurned( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let activeEnergyType = ReadTypes.activeEnergyBurned let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: activeEnergyType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query active energy: \(error.localizedDescription)", error) return } guard let energySamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "totalValue": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let energyData = energySamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.kilocalorie()), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let totalValue = energySamples.reduce(0.0) { total, sample in return total + sample.quantity.doubleValue(for: HKUnit.kilocalorie()) } let result: [String: Any] = [ "data": energyData, "totalValue": totalValue, "count": energyData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getBasalEnergyBurned( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let basalEnergyType = ReadTypes.basalEnergyBurned let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: basalEnergyType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query basal energy: \(error.localizedDescription)", error) return } guard let energySamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "totalValue": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let energyData = energySamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.kilocalorie()), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let totalValue = energySamples.reduce(0.0) { total, sample in return total + sample.quantity.doubleValue(for: HKUnit.kilocalorie()) } let result: [String: Any] = [ "data": energyData, "totalValue": totalValue, "count": energyData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getAppleExerciseTime( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let exerciseType = ReadTypes.appleExerciseTime let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: exerciseType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query exercise time: \(error.localizedDescription)", error) return } guard let exerciseSamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "totalValue": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let exerciseData = exerciseSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.minute()), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let totalValue = exerciseSamples.reduce(0.0) { total, sample in return total + sample.quantity.doubleValue(for: HKUnit.minute()) } let result: [String: Any] = [ "data": exerciseData, "totalValue": totalValue, "count": exerciseData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getAppleStandTime( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let standType = ReadTypes.appleStandTime let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: standType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query stand time: \(error.localizedDescription)", error) return } guard let standSamples = samples as? [HKCategorySample] else { resolver([ "data": [], "totalHours": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let standData = standSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.value, "categoryType": self?.standValueToString(sample.value) ?? "unknown", "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } // Count hours where user stood (value = 0 means stood, value = 1 means idle) let standHours = standSamples.filter { $0.value == HKCategoryValueAppleStandHour.stood.rawValue }.count let result: [String: Any] = [ "data": standData, "totalHours": standHours, "count": standData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getActivitySummary( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let calendar = Calendar.current var startDateComponents = calendar.dateComponents([.day, .month, .year], from: startDate) var endDateComponents = calendar.dateComponents([.day, .month, .year], from: endDate) // HealthKit requires DateComponents to have a calendar startDateComponents.calendar = calendar endDateComponents.calendar = calendar let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: startDateComponents, end: endDateComponents) let query = HKActivitySummaryQuery(predicate: predicate) { [weak self] (query, summaries, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query activity summary: \(error.localizedDescription)", error) return } guard let summaries = summaries, !summaries.isEmpty else { resolver([]) return } let summaryData = summaries.map { summary in // 获取对应日期的 DateComponents let summaryDateComponents = calendar.dateComponents([.day, .month, .year], from: startDate) return [ "activeEnergyBurned": summary.activeEnergyBurned.doubleValue(for: HKUnit.kilocalorie()), "activeEnergyBurnedGoal": summary.activeEnergyBurnedGoal.doubleValue(for: HKUnit.kilocalorie()), "appleExerciseTime": summary.appleExerciseTime.doubleValue(for: HKUnit.minute()), "appleExerciseTimeGoal": summary.appleExerciseTimeGoal.doubleValue(for: HKUnit.minute()), "appleStandHours": summary.appleStandHours.doubleValue(for: HKUnit.count()), "appleStandHoursGoal": summary.appleStandHoursGoal.doubleValue(for: HKUnit.count()), "dateComponents": [ "day": summaryDateComponents.day ?? 0, "month": summaryDateComponents.month ?? 0, "year": summaryDateComponents.year ?? 0 ] ] as [String : Any] } resolver(summaryData) } } healthStore.execute(query) } @objc func getHeartRateVariabilitySamples( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let hrvType = ReadTypes.heartRateVariability let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit let query = HKSampleQuery(sampleType: hrvType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query HRV: \(error.localizedDescription)", error) return } guard let hrvSamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let hrvData = hrvSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.secondUnit(with: .milli)), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let result: [String: Any] = [ "data": hrvData, "count": hrvData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getOxygenSaturationSamples( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let oxygenType = ReadTypes.oxygenSaturation let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit let query = HKSampleQuery(sampleType: oxygenType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query oxygen saturation: \(error.localizedDescription)", error) return } guard let oxygenSamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let oxygenData = oxygenSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.percent()), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let result: [String: Any] = [ "data": oxygenData, "count": oxygenData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getHeartRateSamples( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let heartRateType = ReadTypes.heartRate let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit let query = HKSampleQuery(sampleType: heartRateType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query heart rate: \(error.localizedDescription)", error) return } guard let heartRateSamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let heartRateData = heartRateSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit(from: "count/min")), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let result: [String: Any] = [ "data": heartRateData, "count": heartRateData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getStepCount( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let stepType = ReadTypes.stepCount let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: stepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query step count: \(error.localizedDescription)", error) return } guard let stepSamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "totalValue": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let stepData = stepSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.count()), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let totalValue = stepSamples.reduce(0.0) { total, sample in return total + sample.quantity.doubleValue(for: HKUnit.count()) } let result: [String: Any] = [ "data": stepData, "totalValue": totalValue, "count": stepData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getDailyStepCountSamples( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let stepType = ReadTypes.stepCount let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } // Create date components for statistics collection interval (hourly) let calendar = Calendar.current let anchorDate = calendar.startOfDay(for: startDate) let interval = DateComponents(hour: 1) let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let query = HKStatisticsCollectionQuery( quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: interval ) query.initialResultsHandler = { [weak self] query, results, error in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query hourly step count: \(error.localizedDescription)", error) return } guard let results = results else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } var hourlyData: [[String: Any]] = [] results.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in let value = statistics.sumQuantity()?.doubleValue(for: HKUnit.count()) ?? 0 let hourData: [String: Any] = [ "startDate": self?.dateToISOString(statistics.startDate) ?? "", "endDate": self?.dateToISOString(statistics.endDate) ?? "", "value": value, "hour": calendar.component(.hour, from: statistics.startDate) ] hourlyData.append(hourData) } let result: [String: Any] = [ "data": hourlyData, "count": hourlyData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } // MARK: - Helper Methods private func authorizationStatusToString(_ status: HKAuthorizationStatus) -> String { switch status { case .notDetermined: return "notDetermined" case .sharingDenied: return "denied" case .sharingAuthorized: return "authorized" @unknown default: return "unknown" } } private func sleepValueToString(_ value: Int) -> String { switch value { case HKCategoryValueSleepAnalysis.inBed.rawValue: return "inBed" case HKCategoryValueSleepAnalysis.asleepUnspecified.rawValue: return "asleep" case HKCategoryValueSleepAnalysis.awake.rawValue: return "awake" case HKCategoryValueSleepAnalysis.asleepCore.rawValue: return "core" case HKCategoryValueSleepAnalysis.asleepDeep.rawValue: return "deep" case HKCategoryValueSleepAnalysis.asleepREM.rawValue: return "rem" default: return "unknown" } } private func standValueToString(_ value: Int) -> String { switch value { case HKCategoryValueAppleStandHour.stood.rawValue: return "stood" case HKCategoryValueAppleStandHour.idle.rawValue: return "idle" default: return "unknown" } } private func parseDate(from string: String?) -> Date? { guard let string = string else { return nil } let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.date(from: string) } private func dateToISOString(_ date: Date) -> String { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter.string(from: date) } // MARK: - Hourly Data Methods @objc func getHourlyActiveEnergyBurned( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let activeEnergyType = ReadTypes.activeEnergyBurned let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let calendar = Calendar.current let anchorDate = calendar.startOfDay(for: startDate) let interval = DateComponents(hour: 1) let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let query = HKStatisticsCollectionQuery( quantityType: activeEnergyType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: interval ) query.initialResultsHandler = { [weak self] query, results, error in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query hourly active energy: \(error.localizedDescription)", error) return } guard let results = results else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } var hourlyData: [[String: Any]] = [] results.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in let value = statistics.sumQuantity()?.doubleValue(for: HKUnit.kilocalorie()) ?? 0 let hourData: [String: Any] = [ "startDate": self?.dateToISOString(statistics.startDate) ?? "", "endDate": self?.dateToISOString(statistics.endDate) ?? "", "value": value, "hour": calendar.component(.hour, from: statistics.startDate) ] hourlyData.append(hourData) } let result: [String: Any] = [ "data": hourlyData, "count": hourlyData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getHourlyExerciseTime( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let exerciseType = ReadTypes.appleExerciseTime let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let calendar = Calendar.current let anchorDate = calendar.startOfDay(for: startDate) let interval = DateComponents(hour: 1) let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let query = HKStatisticsCollectionQuery( quantityType: exerciseType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: interval ) query.initialResultsHandler = { [weak self] query, results, error in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query hourly exercise time: \(error.localizedDescription)", error) return } guard let results = results else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } var hourlyData: [[String: Any]] = [] results.enumerateStatistics(from: startDate, to: endDate) { statistics, stop in let value = statistics.sumQuantity()?.doubleValue(for: HKUnit.minute()) ?? 0 let hourData: [String: Any] = [ "startDate": self?.dateToISOString(statistics.startDate) ?? "", "endDate": self?.dateToISOString(statistics.endDate) ?? "", "value": value, "hour": calendar.component(.hour, from: statistics.startDate) ] hourlyData.append(hourData) } let result: [String: Any] = [ "data": hourlyData, "count": hourlyData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } @objc func getHourlyStandHours( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let standType = ReadTypes.appleStandTime let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let calendar = Calendar.current let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true) let query = HKSampleQuery(sampleType: standType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query hourly stand hours: \(error.localizedDescription)", error) return } guard let standSamples = samples as? [HKCategorySample] else { resolver([ "data": [], "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } // 初始化24小时数据 var hourlyData: [[String: Any]] = [] // 为每个小时创建数据结构 for hour in 0..<24 { let hourStart = calendar.date(byAdding: .hour, value: hour, to: calendar.startOfDay(for: startDate))! let hourEnd = calendar.date(byAdding: .hour, value: hour + 1, to: calendar.startOfDay(for: startDate))! // 检查该小时是否有站立记录 let standSamplesInHour = standSamples.filter { sample in return sample.startDate >= hourStart && sample.startDate < hourEnd && sample.value == HKCategoryValueAppleStandHour.stood.rawValue } let hasStood = standSamplesInHour.count > 0 ? 1 : 0 let hourData: [String: Any] = [ "startDate": self?.dateToISOString(hourStart) ?? "", "endDate": self?.dateToISOString(hourEnd) ?? "", "value": hasStood, "hour": hour ] hourlyData.append(hourData) } let result: [String: Any] = [ "data": hourlyData, "count": hourlyData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } // MARK: - Water Intake Methods @objc func saveWaterIntakeToHealthKit( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } // Parse parameters guard let amount = options["amount"] as? Double else { rejecter("INVALID_PARAMETERS", "Amount is required", nil) return } let recordedAt: Date if let recordedAtString = options["recordedAt"] as? String, let date = parseDate(from: recordedAtString) { recordedAt = date } else { recordedAt = Date() } let waterType = WriteTypes.dietaryWater // Create quantity sample let quantity = HKQuantity(unit: HKUnit.literUnit(with: .milli), doubleValue: amount) let sample = HKQuantitySample( type: waterType, quantity: quantity, start: recordedAt, end: recordedAt, metadata: nil ) // Save to HealthKit healthStore.save(sample) { [weak self] (success, error) in DispatchQueue.main.async { if let error = error { rejecter("SAVE_ERROR", "Failed to save water intake: \(error.localizedDescription)", error) return } if success { let result: [String: Any] = [ "success": true, "amount": amount, "recordedAt": self?.dateToISOString(recordedAt) ?? "" ] resolver(result) } else { rejecter("SAVE_FAILED", "Failed to save water intake", nil) } } } } @objc func getWaterIntakeFromHealthKit( _ options: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { guard HKHealthStore.isHealthDataAvailable() else { rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) return } let waterType = HKObjectType.quantityType(forIdentifier: .dietaryWater)! // Parse date range let startDate: Date if let startString = options["startDate"] as? String, let d = parseDate(from: startString) { startDate = d } else { startDate = Calendar.current.startOfDay(for: Date()) } let endDate: Date if let endString = options["endDate"] as? String, let d = parseDate(from: endString) { endDate = d } else { endDate = Date() } let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) let query = HKSampleQuery(sampleType: waterType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]) { [weak self] (query, samples, error) in DispatchQueue.main.async { if let error = error { rejecter("QUERY_ERROR", "Failed to query water intake: \(error.localizedDescription)", error) return } guard let waterSamples = samples as? [HKQuantitySample] else { resolver([ "data": [], "totalAmount": 0, "count": 0, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ]) return } let waterData = waterSamples.map { sample in [ "id": sample.uuid.uuidString, "startDate": self?.dateToISOString(sample.startDate) ?? "", "endDate": self?.dateToISOString(sample.endDate) ?? "", "value": sample.quantity.doubleValue(for: HKUnit.literUnit(with: .milli)), "source": [ "name": sample.sourceRevision.source.name, "bundleIdentifier": sample.sourceRevision.source.bundleIdentifier ], "metadata": sample.metadata ?? [:] ] as [String : Any] } let totalAmount = waterSamples.reduce(0.0) { total, sample in return total + sample.quantity.doubleValue(for: HKUnit.literUnit(with: .milli)) } let result: [String: Any] = [ "data": waterData, "totalAmount": totalAmount, "count": waterData.count, "startDate": self?.dateToISOString(startDate) ?? "", "endDate": self?.dateToISOString(endDate) ?? "" ] resolver(result) } } healthStore.execute(query) } } // end class