feat: 支持原生模块健康数据
This commit is contained in:
260
ios/OutLive/HealthKitManager.swift
Normal file
260
ios/OutLive/HealthKitManager.swift
Normal file
@@ -0,0 +1,260 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user