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

@@ -3,14 +3,14 @@ PODS:
- DoubleConversion (1.1.6)
- EXApplication (7.0.7):
- ExpoModulesCore
- EXConstants (18.0.8):
- EXConstants (18.0.9):
- ExpoModulesCore
- EXImageLoader (6.0.0):
- ExpoModulesCore
- React-Core
- EXNotifications (0.32.11):
- ExpoModulesCore
- Expo (54.0.7):
- Expo (54.0.8):
- boost
- DoubleConversion
- ExpoModulesCore
@@ -41,7 +41,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- ExpoAppleAuthentication (8.0.6):
- ExpoAppleAuthentication (8.0.7):
- ExpoModulesCore
- ExpoAsset (12.0.8):
- ExpoModulesCore
@@ -49,7 +49,7 @@ PODS:
- ExpoModulesCore
- ExpoBlur (15.0.7):
- ExpoModulesCore
- ExpoCamera (17.0.7):
- ExpoCamera (17.0.8):
- ExpoModulesCore
- ZXingObjC/OneD
- ZXingObjC/PDF417
@@ -57,11 +57,11 @@ PODS:
- ExpoModulesCore
- ExpoFont (14.0.8):
- ExpoModulesCore
- ExpoGlassEffect (0.1.3):
- ExpoGlassEffect (0.1.4):
- ExpoModulesCore
- ExpoHaptics (15.0.6):
- ExpoHaptics (15.0.7):
- ExpoModulesCore
- ExpoHead (6.0.4):
- ExpoHead (6.0.6):
- ExpoModulesCore
- RNScreens
- ExpoImage (3.0.8):
@@ -71,15 +71,15 @@ PODS:
- SDWebImageAVIFCoder (~> 0.11.0)
- SDWebImageSVGCoder (~> 1.7.0)
- SDWebImageWebPCoder (~> 0.14.6)
- ExpoImagePicker (17.0.7):
- ExpoImagePicker (17.0.8):
- ExpoModulesCore
- ExpoKeepAwake (15.0.7):
- ExpoModulesCore
- ExpoLinearGradient (15.0.6):
- ExpoLinearGradient (15.0.7):
- ExpoModulesCore
- ExpoLinking (8.0.8):
- ExpoModulesCore
- ExpoModulesCore (3.0.15):
- ExpoModulesCore (3.0.16):
- boost
- DoubleConversion
- fast_float
@@ -108,19 +108,19 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- ExpoQuickActions (5.0.0):
- ExpoQuickActions (6.0.0):
- ExpoModulesCore
- ExpoSplashScreen (31.0.8):
- ExpoSplashScreen (31.0.10):
- ExpoModulesCore
- ExpoSQLite (16.0.8):
- ExpoModulesCore
- ExpoSymbols (1.0.6):
- ExpoSymbols (1.0.7):
- ExpoModulesCore
- ExpoSystemUI (6.0.7):
- ExpoModulesCore
- ExpoUI (0.2.0-beta.2):
- ExpoUI (0.2.0-beta.3):
- ExpoModulesCore
- ExpoWebBrowser (15.0.6):
- ExpoWebBrowser (15.0.7):
- ExpoModulesCore
- EXTaskManager (14.0.7):
- ExpoModulesCore
@@ -2012,7 +2012,7 @@ PODS:
- Yoga
- react-native-voice (3.2.4):
- React-Core
- react-native-webview (13.15.0):
- react-native-webview (13.16.0):
- boost
- DoubleConversion
- fast_float
@@ -2604,7 +2604,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNCPicker (2.11.1):
- RNCPicker (2.11.2):
- boost
- DoubleConversion
- fast_float
@@ -2632,7 +2632,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNDateTimePicker (8.4.4):
- RNDateTimePicker (8.4.5):
- boost
- DoubleConversion
- fast_float
@@ -2844,7 +2844,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNSentry (6.20.0):
- RNSentry (7.0.1):
- boost
- DoubleConversion
- fast_float
@@ -2874,7 +2874,7 @@ PODS:
- Sentry/HybridSDK (= 8.53.2)
- SocketRocket
- Yoga
- RNSVG (15.12.1):
- RNSVG (15.13.0):
- boost
- DoubleConversion
- fast_float
@@ -2900,10 +2900,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.12.1)
- RNSVG/common (= 15.13.0)
- SocketRocket
- Yoga
- RNSVG/common (15.12.1):
- RNSVG/common (15.13.0):
- boost
- DoubleConversion
- fast_float
@@ -3020,10 +3020,10 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- SDWebImageAVIFCoder (0.11.0):
- SDWebImage (5.21.2):
- SDWebImage/Core (= 5.21.2)
- SDWebImage/Core (5.21.2)
- SDWebImageAVIFCoder (0.11.1):
- libavif/core (>= 0.11.0)
- SDWebImage (~> 5.10)
- SDWebImageSVGCoder (1.7.0):
@@ -3425,33 +3425,33 @@ SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXApplication: 296622817d459f46b6c5fe8691f4aac44d2b79e7
EXConstants: 7e4654405af367ff908c863fe77a8a22d60bd37d
EXConstants: a95804601ee4a6aa7800645f9b070d753b1142b3
EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05
EXNotifications: 7a2975f4e282b827a0bc78bb1d232650cb569bbd
Expo: b7d4314594ebd7fe5eefd1a06c3b0d92b718cde0
ExpoAppleAuthentication: 9eb1ec7213ee9c9797951df89975136db89bf8ac
Expo: f75c4161ba6b82f264daee5f52a50ac2a55d6d67
ExpoAppleAuthentication: bc9de6e9ff3340604213ab9031d4c4f7f802623e
ExpoAsset: 84810d6fed8179f04d4a7a4a6b37028bbd726e26
ExpoBackgroundTask: 22ed53b129d4d5e15c39be9fa68e45d25f6781a1
ExpoBlur: 2dd8f64aa31f5d405652c21d3deb2d2588b1852f
ExpoCamera: ae1d6691b05b753261a845536d2b19a9788a8750
ExpoCamera: e75f6807a2c047f3338bbadd101af4c71a1d13a5
ExpoFileSystem: 4fb06865906e781329eb67166bd64fc4749c3019
ExpoFont: 86ceec09ffed1c99cfee36ceb79ba149074901b5
ExpoGlassEffect: e48c949ee7dcf2072cca31389bf8fa776c1727a0
ExpoHaptics: e0912a9cf05ba958eefdc595f1990b8f89aa1f3f
ExpoHead: 2aad68c730f967d2533599dabb64d1d2cd9f765a
ExpoGlassEffect: 744bf0c58c26a1b0212dff92856be07b98d01d8c
ExpoHaptics: 807476b0c39e9d82b7270349d6487928ce32df84
ExpoHead: 78f14a8573ae5b882123b272c0af20a80bfa58f6
ExpoImage: e88f500585913969b930e13a4be47277eb7c6de8
ExpoImagePicker: 66195293e95879fa5ee3eb1319f10b5de0ffccbb
ExpoImagePicker: d251aab45a1b1857e4156fed88511b278b4eee1c
ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe
ExpoLinearGradient: 74d67832cdb0d2ef91f718d50dd82b273ce2812e
ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27
ExpoLinking: f051f28e50ea9269ff539317c166adec81d9342d
ExpoModulesCore: 5d150c790fb491ab10fe431fb794014af841258f
ExpoQuickActions: fdbda7f5874aed3dd2b1d891ec00ab3300dc7541
ExpoSplashScreen: 1665809071bd907c6fdbfd9c09583ee4d51b41d4
ExpoModulesCore: 654d2976c18a4a764a528928e73c4a25c8eb0e5a
ExpoQuickActions: 31a70aa6a606128de4416a4830e09cfabfe6667f
ExpoSplashScreen: cbb839de72110dea1851dd3e85080b7923af2540
ExpoSQLite: 7fa091ba5562474093fef09be644161a65e11b3f
ExpoSymbols: 3efee6865b1955fe3805ca88b36e8674ce6970dd
ExpoSymbols: 1ae04ce686de719b9720453b988d8bc5bf776c68
ExpoSystemUI: 6cd74248a2282adf6dec488a75fa532d69dee314
ExpoUI: 0f109b0549d1ae2fd955d3b8733b290c5cdeec7e
ExpoWebBrowser: 84d4438464d9754a4c1f1eaa97cd747f3752187e
ExpoUI: 68238da1f16a814f77bc64712a269440174ee898
ExpoWebBrowser: 533bc2a1b188eec1c10e4926decf658f1687b5e5
EXTaskManager: cf225704fab8de8794a6f57f7fa41a90c0e2cd47
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
FBLazyVector: 941bef1c8eeabd9fe1f501e30a5220beee913886
@@ -3500,7 +3500,7 @@ SPEC CHECKSUMS:
react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
react-native-voice: 908a0eba96c8c3d643e4f98b7232c6557d0a6f9c
react-native-webview: 4cbb7f05f2c50671a7dcff4012d3e85faad271e4
react-native-webview: 654f794a7686b47491cf43aa67f7f428bea00eed
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510
@@ -3535,18 +3535,18 @@ SPEC CHECKSUMS:
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82
RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299
RNCPicker: 66c392786945ecee5275242c148e6a4601221d3a
RNDateTimePicker: cda4c045beca864cebb3209ef9cc4094f974864c
RNCPicker: a7e5555ebf53e17e06c1fde62195cf07b685d26c
RNDateTimePicker: 113004837aad399a525cd391ac70b7951219ff2f
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
RNPurchases: 1bc60e3a69af65d9cfe23967328421dd1df1763c
RNReanimated: 9de34f0313c4177a34c079ca9fce6f1f278bff24
RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c
RNSentry: f2c39f1113e22413c9bb6e3faa6b27f110d95eaf
RNSVG: 6f39605a4c4d200b11435c35bd077553c6b5963a
RNSentry: 6c63debc7b22a00cbf7d1c9ed8de43e336216545
RNSVG: 6c39befcfad06eec55b40c19a030b2d9eca63334
RNWorklets: ad0606bee2a8103c14adb412149789c60b72bfb2
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -14,6 +14,8 @@
7996A1192E6FB82300371142 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7996A1182E6FB82300371142 /* WidgetKit.framework */; };
7996A11B2E6FB82300371142 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7996A11A2E6FB82300371142 /* SwiftUI.framework */; };
7996A12C2E6FB82300371142 /* WaterWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7996A1172E6FB82300371142 /* WaterWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
79B2CB032E7ABBC400B51753 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB012E7ABBC400B51753 /* HealthKitManager.swift */; };
79B2CB042E7ABBC400B51753 /* HealthKitManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 79B2CB022E7ABBC400B51753 /* HealthKitManager.m */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
DC3BFC72D3A68C7493D5B44A /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
@@ -52,6 +54,8 @@
7996A1182E6FB82300371142 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
7996A11A2E6FB82300371142 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
7996A1322E6FB84A00371142 /* WaterWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WaterWidgetExtension.entitlements; sourceTree = "<group>"; };
79B2CB012E7ABBC400B51753 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HealthKitManager.swift; path = digitalpilates/HealthKitManager.swift; sourceTree = "<group>"; };
79B2CB022E7ABBC400B51753 /* HealthKitManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HealthKitManager.m; path = digitalpilates/HealthKitManager.m; sourceTree = "<group>"; };
7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = digitalpilates/SplashScreen.storyboard; sourceTree = "<group>"; };
@@ -64,7 +68,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
7996A1302E6FB82300371142 /* Exceptions for "WaterWidget" folder in "WaterWidgetExtension" target */ = {
7996A1302E6FB82300371142 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
@@ -74,18 +78,7 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7996A11C2E6FB82300371142 /* WaterWidget */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
7996A1302E6FB82300371142 /* Exceptions for "WaterWidget" folder in "WaterWidgetExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = WaterWidget;
sourceTree = "<group>";
};
7996A11C2E6FB82300371142 /* WaterWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7996A1302E6FB82300371142 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WaterWidget; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -153,6 +146,8 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
79B2CB022E7ABBC400B51753 /* HealthKitManager.m */,
79B2CB012E7ABBC400B51753 /* HealthKitManager.swift */,
7996A1322E6FB84A00371142 /* WaterWidgetExtension.entitlements */,
13B07FAE1A68108700A75B9A /* digitalpilates */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
@@ -450,6 +445,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
79B2CB032E7ABBC400B51753 /* HealthKitManager.swift in Sources */,
79B2CB042E7ABBC400B51753 /* HealthKitManager.m in Sources */,
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
DC3BFC72D3A68C7493D5B44A /* ExpoModulesProvider.swift in Sources */,
);
@@ -490,7 +487,7 @@
);
INFOPLIST_FILE = digitalpilates/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -528,7 +525,7 @@
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
INFOPLIST_FILE = digitalpilates/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -699,10 +696,7 @@
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -757,10 +751,7 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

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>