Files
digital-pilates/ios/OutLive/HealthKitManager.swift
2025-09-18 09:51:37 +08:00

261 lines
8.1 KiB
Swift

//
// HealthKitManager.swift
// digitalpilates
//
// Native module for HealthKit authorization and sleep data access
//
import Foundation
import React
import HealthKit
@objc(HealthKitManager)
class HealthKitManager: NSObject, RCTBridgeModule {
// HealthKit store instance
private let healthStore = HKHealthStore()
static func moduleName() -> String! {
return "HealthKitManager"
}
static func requiresMainQueueSetup() -> Bool {
return true
}
// MARK: - Authorization
@objc
func requestAuthorization(
_ resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
// Check if HealthKit is available on the device
guard HKHealthStore.isHealthDataAvailable() else {
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
return
}
// Define the data types we want to read
let readTypes: Set<HKObjectType> = [
HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!,
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!
]
// Define the data types we want to write (if any)
let writeTypes: Set<HKSampleType> = [
HKObjectType.quantityType(forIdentifier: .bodyMass)!
]
// Request authorization
healthStore.requestAuthorization(toShare: writeTypes, read: readTypes) { [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 {
// Add a small delay to ensure HealthKit has updated permission status
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// Check individual permissions
var permissions: [String: Any] = [:]
for type in readTypes {
let status = self?.healthStore.authorizationStatus(for: type)
let statusString = self?.authorizationStatusToString(status)
permissions[type.identifier] = statusString
}
let result: [String: Any] = [
"success": true,
"permissions": permissions
]
resolver(result)
}
} else {
rejecter("AUTHORIZATION_DENIED", "User denied HealthKit authorization", nil)
}
}
}
}
// MARK: - Permission Status
@objc
func getAuthorizationStatus(
_ resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
// Check if HealthKit is available on the device
guard HKHealthStore.isHealthDataAvailable() else {
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
return
}
// Define the data types we want to check
let readTypes: Set<HKObjectType> = [
HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!,
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!
]
// Check individual permissions
var permissions: [String: Any] = [:]
for type in readTypes {
let status = healthStore.authorizationStatus(for: type)
let statusString = authorizationStatusToString(status)
permissions[type.identifier] = statusString
}
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
}
// Check authorization status for sleep analysis
let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
let authStatus = healthStore.authorizationStatus(for: sleepType)
guard authStatus == .sharingAuthorized else {
rejecter("NOT_AUTHORIZED", "Not authorized to read sleep data", nil)
return
}
// Parse options
let startDate = parseDate(from: options["startDate"] as? String) ?? Calendar.current.date(byAdding: .day, value: -7, to: Date())!
let endDate = parseDate(from: options["endDate"] as? String) ?? Date()
let limit = options["limit"] as? Int ?? 100
// Create predicate for date range
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
// Create sort descriptor to get latest data first
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
// Create query
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([])
return
}
let sleepData = sleepSamples.map { sample in
return [
"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 ?? [:]
]
}
let result: [String: Any] = [
"data": sleepData,
"count": sleepData.count,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
]
resolver(result)
}
}
healthStore.execute(query)
}
// MARK: - Helper Methods
private func authorizationStatusToString(_ status: HKAuthorizationStatus?) -> String {
guard let status = status else { return "notDetermined" }
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 parseDate(from string: String?) -> Date? {
guard let string = string else { return nil }
let formatter = ISO8601DateFormatter()
return formatter.date(from: string)
}
private func dateToISOString(_ date: Date) -> String {
let formatter = ISO8601DateFormatter()
return formatter.string(from: date)
}
}