feat: 支持 healthkit
This commit is contained in:
22
ios/digitalpilates/HealthKitManager.m
Normal file
22
ios/digitalpilates/HealthKitManager.m
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// HealthKitManager.m
|
||||
// digitalpilates
|
||||
//
|
||||
// React Native bridge for HealthKitManager
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(HealthKitManager, NSObject)
|
||||
|
||||
// Authorization method
|
||||
RCT_EXTERN_METHOD(requestAuthorization:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
// Sleep data method
|
||||
RCT_EXTERN_METHOD(getSleepData:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
|
||||
@end
|
||||
216
ios/digitalpilates/HealthKitManager.swift
Normal file
216
ios/digitalpilates/HealthKitManager.swift
Normal file
@@ -0,0 +1,216 @@
|
||||
//
|
||||
// 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 {
|
||||
// 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: - 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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTViewManager.h>
|
||||
Reference in New Issue
Block a user