feat: 集成健康数据功能优化

- 在 app.json 中添加 react-native-health 的配置以启用健康 API
- 在 Explore 页面中重构健康数据加载逻辑,增加加载状态提示
- 更新健康数据获取函数,增强错误处理和日志输出
- 修改 iOS 权限设置,确保健康数据访问权限的正确配置
- 更新 Info.plist 中的健康数据使用说明
This commit is contained in:
richarjiang
2025-08-12 10:50:37 +08:00
parent e87dc87e1b
commit 456f0d93ce
6 changed files with 174 additions and 84 deletions

View File

@@ -35,6 +35,13 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
],
[
"react-native-health",
{
"enableHealthAPI": true,
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。"
}
]
],
"experiments": {

View File

@@ -51,23 +51,38 @@ export default function ExploreScreen() {
// HealthKit: 每次页面聚焦都拉取今日数据
const [stepCount, setStepCount] = useState<number | null>(null);
const [activeCalories, setActiveCalories] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false);
const loadHealthData = async () => {
try {
console.log('=== 开始HealthKit初始化流程 ===');
setIsLoading(true);
const ok = await ensureHealthPermissions();
if (!ok) {
const errorMsg = '无法获取健康权限请确保在真实iOS设备上运行并授权应用访问健康数据';
console.warn(errorMsg);
return;
}
console.log('权限获取成功,开始获取健康数据...');
const data = await fetchTodayHealthData();
console.log('设置UI状态:', data);
setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned));
console.log('=== HealthKit数据获取完成 ===');
} catch (error) {
console.error('HealthKit流程出现异常:', error);
} finally {
setIsLoading(false);
}
};
useFocusEffect(
React.useCallback(() => {
let isActive = true;
const run = async () => {
console.log('HealthKit init start');
const ok = await ensureHealthPermissions();
if (!ok) return;
const data = await fetchTodayHealthData();
if (!isActive) return;
setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned));
};
run();
return () => {
isActive = false;
};
loadHealthData();
}, [])
);
@@ -113,6 +128,24 @@ export default function ExploreScreen() {
{/* 今日报告 标题 */}
<Text style={styles.sectionTitle}></Text>
{/* 健康数据错误提示 */}
{isLoading && (
<View style={styles.errorContainer}>
<Ionicons name="warning-outline" size={20} color="#E54D4D" />
<Text style={styles.errorText}>...</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={loadHealthData} disabled={isLoading}
>
<Ionicons
name="refresh-outline"
size={16}
color={isLoading ? '#9AA3AE' : '#E54D4D'}
/>
</TouchableOpacity>
</View>
)}
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
<View style={styles.metricsRow}>
<View style={[styles.trainingCard, styles.metricsLeft]}>
@@ -127,7 +160,7 @@ export default function ExploreScreen() {
<View style={[styles.metricsRightCard, styles.caloriesCard, { minHeight: 88 }]}>
<Text style={styles.cardTitleSecondary}></Text>
<Text style={styles.caloriesValue}>
{activeCalories != null ? `${activeCalories} 千卡` : '——'}
{isLoading ? '加载中...' : activeCalories != null ? `${activeCalories} 千卡` : '——'}
</Text>
</View>
<View style={[styles.metricsRightCard, styles.stepsCard, { minHeight: 88 }]}>
@@ -135,7 +168,7 @@ export default function ExploreScreen() {
<View style={styles.iconSquare}><Ionicons name="footsteps-outline" size={18} color="#192126" /></View>
<Text style={styles.cardTitle}></Text>
</View>
<Text style={styles.stepsValue}>{stepCount != null ? `${stepCount}/2000` : '——/2000'}</Text>
<Text style={styles.stepsValue}>{isLoading ? '加载中.../2000' : stepCount != null ? `${stepCount}/2000` : '——/2000'}</Text>
<ProgressBar progress={Math.min(1, Math.max(0, (stepCount ?? 0) / 2000))} height={12} trackColor="#FFEBCB" fillColor="#FFC365" />
</View>
</View>
@@ -386,5 +419,24 @@ const styles = StyleSheet.create({
color: '#7A6A42',
fontWeight: '700',
marginBottom: 8,
}
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFE5E5',
borderRadius: 12,
padding: 12,
marginBottom: 16,
},
errorText: {
fontSize: 14,
color: '#E54D4D',
fontWeight: '600',
marginLeft: 8,
flex: 1,
},
retryButton: {
padding: 4,
marginLeft: 8,
},
});

View File

@@ -8,28 +8,28 @@
/* Begin PBXBuildFile section */
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
24A7AF62E3CEAC7DED8D0215 /* libPods-digitalpilates.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E1F9207C56E5E9A17A80BC /* libPods-digitalpilates.a */; };
27B482E4BA415859EA8B0372 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732A3660B00244505674555 /* ExpoModulesProvider.swift */; };
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
676318C8AE606E8604AB1163 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951C0617485F4FEF590D4C5D /* ExpoModulesProvider.swift */; };
7F5A026795C1BF28BEBBFA4E /* libPods-digitalpilates.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FDAB65677BEFC9C3E52449E8 /* libPods-digitalpilates.a */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
E243B50C4648FB8A9C9EEF3B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 905D0A94B379FF06B1A262B2 /* PrivacyInfo.xcprivacy */; };
CBE4D0B57478826DADA47BC9 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C59D7448330DC33ACFEECB09 /* PrivacyInfo.xcprivacy */; };
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
05E1F9207C56E5E9A17A80BC /* libPods-digitalpilates.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-digitalpilates.a"; sourceTree = BUILT_PRODUCTS_DIR; };
031BAB3982A98E78D1493449 /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* digitalpilates.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = digitalpilates.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = digitalpilates/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = digitalpilates/Info.plist; sourceTree = "<group>"; };
905D0A94B379FF06B1A262B2 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
951C0617485F4FEF590D4C5D /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
7398BB3C47424238F7BEF8F9 /* Pods-digitalpilates.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.release.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.release.xcconfig"; sourceTree = "<group>"; };
8732A3660B00244505674555 /* 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>"; };
B8247601A9965B9A71737DFD /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
E41FEE326E770C4798A5541A /* Pods-digitalpilates.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.release.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.release.xcconfig"; sourceTree = "<group>"; };
C59D7448330DC33ACFEECB09 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = digitalpilates/AppDelegate.swift; sourceTree = "<group>"; };
F11748442D0722820044C1D9 /* digitalpilates-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "digitalpilates-Bridging-Header.h"; path = "digitalpilates/digitalpilates-Bridging-Header.h"; sourceTree = "<group>"; };
FDAB65677BEFC9C3E52449E8 /* libPods-digitalpilates.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-digitalpilates.a"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -37,7 +37,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
24A7AF62E3CEAC7DED8D0215 /* libPods-digitalpilates.a in Frameworks */,
7F5A026795C1BF28BEBBFA4E /* libPods-digitalpilates.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -53,7 +53,7 @@
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
905D0A94B379FF06B1A262B2 /* PrivacyInfo.xcprivacy */,
C59D7448330DC33ACFEECB09 /* PrivacyInfo.xcprivacy */,
);
name = digitalpilates;
sourceTree = "<group>";
@@ -62,27 +62,19 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
05E1F9207C56E5E9A17A80BC /* libPods-digitalpilates.a */,
FDAB65677BEFC9C3E52449E8 /* libPods-digitalpilates.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
4F7DF3E41466E05C57FED62C /* digitalpilates */ = {
7B79F82457416491183F46D6 /* digitalpilates */ = {
isa = PBXGroup;
children = (
951C0617485F4FEF590D4C5D /* ExpoModulesProvider.swift */,
8732A3660B00244505674555 /* ExpoModulesProvider.swift */,
);
name = digitalpilates;
sourceTree = "<group>";
};
5E539D2C9AB8910307FD73AB /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
4F7DF3E41466E05C57FED62C /* digitalpilates */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
@@ -97,8 +89,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
E08D8B522D8095FEAE625994 /* Pods */,
5E539D2C9AB8910307FD73AB /* ExpoModulesProviders */,
8D14961AE80832AA51F9FFA3 /* Pods */,
F7E6B913F90880BFADBE192E /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
@@ -113,6 +105,16 @@
name = Products;
sourceTree = "<group>";
};
8D14961AE80832AA51F9FFA3 /* Pods */ = {
isa = PBXGroup;
children = (
031BAB3982A98E78D1493449 /* Pods-digitalpilates.debug.xcconfig */,
7398BB3C47424238F7BEF8F9 /* Pods-digitalpilates.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
BB2F792B24A3F905000567C9 /* Supporting */ = {
isa = PBXGroup;
children = (
@@ -122,13 +124,12 @@
path = digitalpilates/Supporting;
sourceTree = "<group>";
};
E08D8B522D8095FEAE625994 /* Pods */ = {
F7E6B913F90880BFADBE192E /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
B8247601A9965B9A71737DFD /* Pods-digitalpilates.debug.xcconfig */,
E41FEE326E770C4798A5541A /* Pods-digitalpilates.release.xcconfig */,
7B79F82457416491183F46D6 /* digitalpilates */,
);
path = Pods;
name = ExpoModulesProviders;
sourceTree = "<group>";
};
/* End PBXGroup section */
@@ -139,13 +140,13 @@
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "digitalpilates" */;
buildPhases = (
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
934267B0C231F8AAB5B2FE7D /* [Expo] Configure project */,
E959B4C3D4071EECDD504F2E /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
E8A50A64ECE6FF559ED244DD /* [CP] Embed Pods Frameworks */,
679509E8E2F87FD68CDC2915 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -195,7 +196,7 @@
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
E243B50C4648FB8A9C9EEF3B /* PrivacyInfo.xcprivacy in Resources */,
CBE4D0B57478826DADA47BC9 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -239,6 +240,24 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
679509E8E2F87FD68CDC2915 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -275,7 +294,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-resources.sh\"\n";
showEnvVarsInLog = 0;
};
934267B0C231F8AAB5B2FE7D /* [Expo] Configure project */ = {
E959B4C3D4071EECDD504F2E /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
@@ -294,24 +313,6 @@
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-digitalpilates/expo-configure-project.sh\"\n";
};
E8A50A64ECE6FF559ED244DD /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -320,7 +321,7 @@
buildActionMask = 2147483647;
files = (
F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
676318C8AE606E8604AB1163 /* ExpoModulesProvider.swift in Sources */,
27B482E4BA415859EA8B0372 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -329,13 +330,12 @@
/* Begin XCBuildConfiguration section */
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B8247601A9965B9A71737DFD /* Pods-digitalpilates.debug.xcconfig */;
baseConfigurationReference = 031BAB3982A98E78D1493449 /* Pods-digitalpilates.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 756WVXJ6MT;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@@ -354,7 +354,7 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates;
PRODUCT_BUNDLE_IDENTIFIER = "digital-pilates";
PRODUCT_NAME = digitalpilates;
SWIFT_OBJC_BRIDGING_HEADER = "digitalpilates/digitalpilates-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -366,13 +366,12 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E41FEE326E770C4798A5541A /* Pods-digitalpilates.release.xcconfig */;
baseConfigurationReference = 7398BB3C47424238F7BEF8F9 /* Pods-digitalpilates.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 756WVXJ6MT;
INFOPLIST_FILE = digitalpilates/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
@@ -386,7 +385,7 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates;
PRODUCT_BUNDLE_IDENTIFIER = "digital-pilates";
PRODUCT_NAME = digitalpilates;
SWIFT_OBJC_BRIDGING_HEADER = "digitalpilates/digitalpilates-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@@ -28,7 +28,7 @@
<key>CFBundleURLSchemes</key>
<array>
<string>digitalpilates</string>
<string>com.anonymous.digitalpilates</string>
<string>digital-pilates</string>
</array>
</dict>
</array>
@@ -45,14 +45,14 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSHealthShareUsageDescription</key>
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
<key>NSHealthUpdateUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to update health info</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>NSHealthShareUsageDescription</key>
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
<key>NSHealthUpdateUsageDescription</key>
<string>应用需要写入健康数据以提供更准确的统计。</string>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>

View File

@@ -4,5 +4,7 @@
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.healthkit.access</key>
<array/>
</dict>
</plist>

View File

@@ -1,4 +1,5 @@
import AppleHealthKit, { HealthKitPermissions } from 'react-native-health';
import type { HealthKitPermissions } from 'react-native-health';
import AppleHealthKit from 'react-native-health';
const PERMISSIONS: HealthKitPermissions = {
permissions: {
@@ -17,37 +18,66 @@ export type TodayHealthData = {
export async function ensureHealthPermissions(): Promise<boolean> {
return new Promise((resolve) => {
console.log('开始初始化HealthKit...');
AppleHealthKit.initHealthKit(PERMISSIONS, (error) => {
if (error) {
console.warn('HealthKit init failed', error);
console.error('HealthKit初始化失败:', error);
// 常见错误处理
if (typeof error === 'string') {
if (error.includes('not available')) {
console.warn('HealthKit不可用 - 可能在模拟器上运行或非iOS设备');
}
}
resolve(false);
return;
}
console.log('HealthKit init success');
console.log('HealthKit初始化成功');
resolve(true);
});
});
}
export async function fetchTodayHealthData(): Promise<TodayHealthData> {
console.log('开始获取今日健康数据...');
const start = new Date();
start.setHours(0, 0, 0, 0);
const options = { startDate: start.toISOString() } as any;
console.log('查询选项:', options);
const steps = await new Promise<number>((resolve) => {
AppleHealthKit.getStepCount(options, (err, res) => {
if (err || !res) return resolve(0);
if (err) {
console.error('获取步数失败:', err);
return resolve(0);
}
if (!res) {
console.warn('步数数据为空');
return resolve(0);
}
console.log('步数数据:', res);
resolve(res.value || 0);
});
});
const calories = await new Promise<number>((resolve) => {
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
if (err || !res) return resolve(0);
if (err) {
console.error('获取消耗卡路里失败:', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('卡路里数据为空或格式错误');
return resolve(0);
}
console.log('卡路里数据:', res);
// library returns value as number in kilocalories
resolve(res[0].value || 0);
resolve(res[0]?.value || 0);
});
});
console.log('今日健康数据获取完成:', { steps, calories });
return { steps, activeEnergyBurned: calories };
}