feat: 支持 healthkit

This commit is contained in:
richarjiang
2025-09-17 18:05:11 +08:00
parent 63ed820e93
commit 6b7776e51d
15 changed files with 1675 additions and 532 deletions

View 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

View 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)
}
}

View File

@@ -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>