Files
digital-pilates/ios/OutLive/HealthKitManager.swift
2025-09-18 22:40:05 +08:00

1065 lines
36 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 restingHeartRate = HKObjectType.quantityType(forIdentifier: .restingHeartRate)!
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 var all: Set<HKObjectType> {
return [sleep, stepCount, heartRate, restingHeartRate, heartRateVariability, activeEnergyBurned, basalEnergyBurned, appleExerciseTime, appleStandTime, oxygenSaturation]
}
}
/// For writing (if needed)
private struct WriteTypes {
static let bodyMass = HKObjectType.quantityType(forIdentifier: .bodyMass)!
static var all: Set<HKSampleType> {
return [bodyMass]
}
}
// 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
let anchoredDateComponents = calendar.dateComponents([.day, .month, .year], from: startDate)
let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: anchoredDateComponents, end: anchoredDateComponents)
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
[
"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": anchoredDateComponents.day ?? 0,
"month": anchoredDateComponents.month ?? 0,
"year": anchoredDateComponents.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)
}
} // end class