261 lines
8.1 KiB
Swift
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)
|
|
}
|
|
}
|