React Native HealthKit步数睡眠数据集成完整指南
概述
Apple HealthKit是iOS设备上最全面的健康数据平台,可访问:
- 步数:每日行走步数
- 睡眠分析:睡眠时长和阶段
- 心率:静息心率和HRV
- 能量消耗:活动卡路里
- 距离:行走/跑步距离
本教程将教你如何使用React Native集成HealthKit。
技术方案
集成方式
| 方案 | 库 | 优点 | 缺点 |
|---|---|---|---|
| react-native-health | react-native-health | 功能完整,支持读写 | 配置复杂 |
| expo-sensors | Expo官方 | 简单易用 | 功能有限 |
| 原生模块 | 自定义Swift | 最大灵活性 | 开发成本高 |
推荐:使用react-native-health
环境设置
安装依赖
code
# 安装react-native-health
npm install react-native-health
# iOS额外依赖
cd ios && pod install
Code collapsed
Info.plist配置
code
<!-- ios/YourApp/Info.plist -->
<key>NSHealthShareUsageDescription</key>
<string>我们需要访问您的健康数据来跟踪步数和睡眠信息</string>
<key>NSHealthUpdateUsageDescription</key>
<string>我们需要写入健康数据来保存您的活动记录</string>
<key>NSHealthClinicalHealthRecordsShareUsageDescription</key>
<string>访问健康记录以提供更准确的健康建议</string>
Code collapsed
权限请求
权限定义
code
// constants/HealthKitPermissions.ts
import { HealthKitPermissions } from 'react-native-health';
export const HEALTH_PERMISSIONS: HealthKitPermissions = {
permissions: {
read: [
'Steps', // 步数
'Distance', // 距离
'ActiveEnergyBurned', // 活动能量消耗
'BasalEnergyBurned', // 基础能量消耗
'HeartRate', // 心率
'RestingHeartRate', // 静息心率
'HeartRateVariability', // 心率变异性
'SleepAnalysis', // 睡眠分析
'SleepAnalysis.INBED', // 上床时间
'SleepAnalysis.ASLEEP', // 入睡时间
'SleepAnalysis.AWAKE', // 醒来时间
],
write: [
'Steps', // 步数
'Distance', // 距离
'ActiveEnergyBurned', // 活动能量消耗
'SleepAnalysis', // 睡眠分析
]
}
};
Code collapsed
请求权限
code
// services/HealthKitService.ts
import ReactNativeHealth, {
HealthKitPermissions,
HealthValue,
HealthActivitySummary
} from 'react-native-health';
class HealthKitService {
private isInitialized = false;
/**
* 初始化HealthKit
*/
async initialize(): Promise<boolean> {
try {
// 检查HealthKit是否可用
const isAvailable = await ReactNativeHealth.isAvailable();
if (!isAvailable) {
console.log('HealthKit不可用');
return false;
}
// 请求权限
const granted = await ReactNativeHealth.initHealthKit(HEALTH_PERMISSIONS);
if (granted) {
this.isInitialized = true;
console.log('HealthKit权限已授予');
return true;
} else {
console.log('HealthKit权限被拒绝');
return false;
}
} catch (error) {
console.error('HealthKit初始化失败:', error);
return false;
}
}
}
Code collapsed
步数数据
查询今日步数
code
/**
* 获取今日步数
*/
async function getTodaySteps(): Promise<number> {
if (!this.isInitialized) {
await this.initialize();
}
const now = new Date();
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate());
try {
const steps = await ReactNativeHealth.getDailyStepCountSamples(
startOfDay.toISOString(),
now.toISOString()
);
if (steps && steps.length > 0) {
// steps数组包含多个样本,取最新值
const latestSteps = steps[steps.length - 1];
return latestSteps.value || 0;
}
return 0;
} catch (error) {
console.error('获取步数失败:', error);
return 0;
}
}
Code collapsed
查询历史步数
code
/**
* 获取历史步数(最近N天)
*/
async function getHistoricalSteps(days: number = 30): Promise<Array<{
date: Date;
steps: number;
distance: number;
calories: number
}>> {
const now = new Date();
const startDate = new Date(now);
startDate.setDate(now.getDate() - days);
try {
// 获取步数
const stepsData = await ReactNativeHealth.getDailyStepCountSamples(
startDate.toISOString(),
now.toISOString()
);
// 获取距离
const distanceData = await ReactNativeHealth.getDistanceSamples(
startDate.toISOString(),
now.toISOString(),
'walking'
);
// 获取活动能量消耗
const caloriesData = await ReactNativeHealth.getActiveEnergyBurnedSamples(
startDate.toISOString(),
now.toISOString()
);
// 合并数据(按日期分组)
const dailyData = new Map();
// 处理步数数据
stepsData?.forEach(step => {
const date = new Date(startDate);
date.setDate(startDate.getDate() + step.day);
if (!dailyData.has(date.toDateString())) {
dailyData.set(date.toDateString(), {
date,
steps: step.value || 0,
distance: 0,
calories: 0
});
} else {
const data = dailyData.get(date.toDateString());
data.steps = step.value || 0;
}
});
// 处理距离和卡路里(类似逻辑)
return Array.from(dailyData.values()).sort((a, b) =>
a.date.getTime() - b.date.getTime()
);
} catch (error) {
console.error('获取历史步数失败:', error);
return [];
}
}
Code collapsed
写入步数数据
code
/**
* 保存步数到HealthKit
*/
async function saveSteps(steps: number, date: Date = new Date()): Promise<boolean> {
try {
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const endOfDay = new Date(startOfDay);
endOfDay.setDate(startOfDay.getDate() + 1);
await ReactNativeHealth.writeHealthData(
'Steps',
'steps',
{
startTime: startOfDay.toISOString(),
endTime: endOfDay.toISOString(),
value: steps
}
);
console.log(`已保存${steps}步到HealthKit`);
return true;
} catch (error) {
console.error('保存步数失败:', error);
return false;
}
}
Code collapsed
睡眠数据
查询睡眠数据
code
/**
* 获取睡眠数据(iOS 16+使用新API)
*/
async function getSleepData(days: number = 7): Promise<Array<{
date: Date;
inBed: Date;
asleep: Date;
wakeUp: Date;
duration: number; // 总时长(分钟)
sleepEfficiency: number // 睡眠效率
}>> {
const now = new Date();
const startDate = new Date(now);
startDate.setDate(now.getDate() - days);
try {
const sleepSamples = await ReactNativeHealth.getSleepSamples(
startDate.toISOString(),
now.toISOString()
);
return sleepSamples?.map(sample => ({
date: new Date(sample.startDate),
inBed: new Date(sample.startDate),
asleep: new Date(sample.asleepDate || sample.startDate),
wakeUp: new Date(sample.endDate),
duration: (sample.endDate - sample.startDate) / 60000,
sleepEfficiency: sample.value || 0
})) || [];
} catch (error) {
console.error('获取睡眠数据失败:', error);
// iOS 16以下使用旧API
return this._getSleepDataLegacy(startDate, now);
}
}
/**
* 兼容旧iOS版本的睡眠数据获取
*/
private async _getSleepDataLegacy(startDate: Date, endDate: Date) {
try {
// 分别获取上床、入睡、醒来时间
const inBedSamples = await ReactNativeHealth.getSleepAnalysisSamples(
startDate.toISOString(),
endDate.toISOString(),
'INBED'
);
const asleepSamples = await ReactNativeHealth.getSleepAnalysisSamples(
startDate.toISOString(),
endDate.toISOString(),
'ASLEEP'
);
const awakeSamples = await ReactNativeHealth.getSleepAnalysisSamples(
startDate.toISOString(),
endDate.toISOString(),
'AWAKE'
);
// 合并数据(简化版)
const sleepData = [];
for (let i = 0; i < inBedSamples.length; i++) {
const inBed = inBedSamples[i];
const asleep = asleepSamples[i];
const awake = awakeSamples[i];
if (inBed && awake) {
sleepData.push({
date: new Date(inBed.startDate),
inBed: new Date(inBed.startDate),
asleep: asleep ? new Date(asleep.startDate) : new Date(inBed.startDate),
wakeUp: new Date(awake.endDate),
duration: (awake.endDate - inBed.startDate) / 60000,
sleepEfficiency: 0.85 // 默认值
});
}
}
return sleepData;
} catch (error) {
console.error('获取睡眠数据失败:', error);
return [];
}
}
Code collapsed
写入睡眠数据
code
/**
* 保存睡眠数据到HealthKit
*/
async function saveSleepData(params: {
bedTime: Date;
wakeTime: Date;
sleepQuality: number; // 1-5
}): Promise<boolean> {
try {
// 写入上床时间
await ReactNativeHealth.writeHealthData(
'SleepAnalysis',
'INBED',
{
startTime: params.bedTime.toISOString(),
endTime: params.wakeTime.toISOString()
}
);
// 写入睡眠时间
await ReactNativeHealth.writeHealthData(
'SleepAnalysis',
'ASLEEP',
{
startTime: params.bedTime.toISOString(),
endTime: params.wakeTime.toISOString(),
value: params.sleepQuality / 5 // 转换为0-1范围
}
);
// 写入醒来时间
await ReactNativeHealth.writeHealthData(
'SleepAnalysis',
'AWAKE',
{
startTime: params.wakeTime.toISOString(),
endTime: new Date(params.wakeTime.getTime() + 60000).toISOString()
}
);
console.log('睡眠数据已保存到HealthKit');
return true;
} catch (error) {
console.error('保存睡眠数据失败:', error);
return false;
}
}
Code collapsed
React Hook集成
useHealthData Hook
code
// hooks/useHealthData.ts
import { useState, useEffect } from 'react';
import { HealthKitService } from '../services/HealthKitService';
interface HealthData {
todaySteps: number;
todayDistance: number;
todayCalories: number;
lastSleep?: {
duration: number;
efficiency: number;
};
}
export function useHealthData() {
const [data, setData] = useState<HealthData>({
todaySteps: 0,
todayDistance: 0,
todayCalories: 0
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadHealthData();
// 每分钟更新一次
const interval = setInterval(loadHealthData, 60000);
return () => clearInterval(interval);
}, []);
const loadHealthData = async () => {
try {
setLoading(true);
const service = new HealthKitService();
await service.initialize();
// 获取今日数据
const todaySteps = await service.getTodaySteps();
const todayDistance = await service.getTodayDistance();
const todayCalories = await service.getTodayCalories();
const lastSleep = await service.getLastSleep();
setData({
todaySteps,
todayDistance,
todayCalories,
lastSleep
});
setError(null);
} catch (err) {
setError('加载健康数据失败');
console.error(err);
} finally {
setLoading(false);
}
};
return { data, loading, error, refetch: loadHealthData };
}
Code collapsed
UI组件
code
// components/HealthDataCard.tsx
import React from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useHealthData } from '../hooks/useHealthData';
export function HealthDataCard() {
const { data, loading, error } = useHealthData();
if (loading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
</View>
);
}
if (error) {
return (
<View style={styles.container}>
<Text style={styles.error}>{error}</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>今日活动</Text>
{/* 步数 */}
<View style={styles.metric}>
<Text style={styles.metricLabel}>步数</Text>
<Text style={styles.metricValue}>
{data.todaySteps.toLocaleString()}
</Text>
<Text style={styles.metricUnit}>步</Text>
</View>
{/* 距离 */}
<View style={styles.metric}>
<Text style={styles.metricLabel}>距离</Text>
<Text style={styles.metricValue}>
{(data.todayDistance / 1000).toFixed(1)}
</Text>
<Text style={styles.metricUnit}>公里</Text>
</View>
{/* 卡路里 */}
<View style={styles.metric}>
<Text style={styles.metricLabel}>活动消耗</Text>
<Text style={styles.metricValue}>
{data.todayCalories.toFixed(0)}
</Text>
<Text style={styles.metricUnit}>千卡</Text>
</View>
{/* 睡眠 */}
{data.lastSleep && (
<View style={styles.metric}>
<Text style={styles.metricLabel}>昨晚睡眠</Text>
<Text style={styles.metricValue}>
{data.lastSleep.duration.toFixed(0)}
</Text>
<Text style={styles.metricUnit}>分钟</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
borderRadius: 10,
padding: 15,
margin: 10,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
},
metric: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
metricLabel: {
fontSize: 14,
color: '#666',
width: 100,
},
metricValue: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
flex: 1,
},
metricUnit: {
fontSize: 12,
color: '#999',
marginLeft: 5,
},
error: {
fontSize: 14,
color: 'red',
textAlign: 'center',
},
});
Code collapsed
后台同步
观察者模式
code
// services/HealthKitObserver.ts
import { NativeEventEmitter, NativeModules } from 'react-native';
const { HealthKitObserver } = NativeModules;
const emitter = new NativeEventEmitter(HealthKitObserver);
class HealthKitObserverService {
private listeners: Map<string, any> = new Map();
/**
* 订阅健康数据变化
*/
subscribe(healthType: string, callback: (data: any) => void) {
const listener = emitter.addListener(`HealthKit:${healthType}`, callback);
this.listeners.set(healthType, listener);
// 启动观察
HealthKitObserver.startObserving(healthType);
}
/**
* 取消订阅
*/
unsubscribe(healthType: string) {
const listener = this.listeners.get(healthType);
if (listener) {
listener.remove();
this.listeners.delete(healthType);
// 停止观察
HealthKitObserver.stopObserving(healthType);
}
}
/**
* 取消所有订阅
*/
unsubscribeAll() {
this.listeners.forEach((listener, type) => {
listener.remove();
HealthKitObserver.stopObserving(type);
});
this.listeners.clear();
}
}
export const healthKitObserver = new HealthKitObserverService();
Code collapsed
关键要点
- 权限请求必须在Info.plist声明:iOS要求
- iOS 16+有新睡眠API:需适配
- 数据量限制:最多读取最近30天
- 后台观察需要权限:设置后台模式
- 数据写入需谨慎:避免重复写入
常见问题
Android怎么办?
使用Google Fit API:
code
import { GoogleFit } from 'react-native-google-fit';
Code collapsed
数据不准确?
可能原因:
- 用户未穿戴设备
- 设备未同步
- 不同应用写入冲突
权限被拒绝?
处理策略:
- 引导用户到设置页面
- 说明数据用途
- 提供部分功能降级
参考资料
- Apple HealthKit文档
- react-native-health文档
- 健康数据隐私指南
- 后台模式配置
发布日期:2026年3月8日 最后更新:2026年3月8日