React Native 与 Apple HealthKit 集成完整指南
概述
Apple HealthKit 是 iOS 平台的健康数据存储中心,集中管理用户的健康和健身数据。集成 HealthKit 可以让你的 React Native 应用读取和写入各种健康数据,与苹果生态系统无缝集成。
HealthKit 架构
核心概念
code
┌─────────────────────────────────────────┐
│ Health App │
│ (用户查看和管理健康数据的主界面) │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ HealthKit Framework │
│ ┌───────────────────────────────────┐ │
│ │ HealthStore (数据存储) │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ HKObjectType (数据类型) │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ HKQuery (查询接口) │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ 第三方应用 (你的 App) │
│ - 请求权限 │
│ - 读取数据 │
│ - 写入数据 │
└─────────────────────────────────────────┘
Code collapsed
项目配置
1. 安装依赖
code
npm install @react-native-health/healthkit react-native-health
npm install react-native-permissions
Code collapsed
2. iOS 配置
在 ios/YourApp/Info.plist 中添加权限描述:
code
<key>NSHealthShareUsageDescription</key>
<string>我们需要访问您的健康数据来追踪您的健身目标和进度。</string>
<key>NSHealthUpdateUsageDescription</key>
<string>我们需要写入健康数据来保存您的运动记录。</string>
<key>NSHealthClinicalHealthRecordsShareUsageDescription</key>
<string>我们需要访问您的临床健康记录以提供更全面的健康分析。</string>
Code collapsed
启用 HealthKit 能力:
- 在 Xcode 中打开项目
- 选择 Target → Signing & Capabilities
- 添加 Capability → HealthKit
3. 数据类型映射
code
// lib/healthKit/types.ts
/**
* HealthKit 数据类型枚举
*/
export enum HKQuantityType {
// 身体测量
HEIGHT = 'HKQuantityTypeIdentifierHeight',
WEIGHT = 'HKQuantityTypeIdentifierBodyMass',
BODY_MASS_INDEX = 'HKQuantityTypeIdentifierBodyMassIndex',
BODY_FAT_PERCENTAGE = 'HKQuantityTypeIdentifierBodyFatPercentage',
LEAN_BODY_MASS = 'HKQuantityTypeIdentifierLeanBodyMass',
// 心血管
HEART_RATE = 'HKQuantityTypeIdentifierHeartRate',
RESTING_HEART_RATE = 'HKQuantityTypeIdentifierRestingHeartRate',
WALKING_HEART_RATE = 'HKQuantityTypeIdentifierWalkingHeartRateAverage',
BLOOD_PRESSURE_SYSTOLIC = 'HKQuantityTypeIdentifierBloodPressureSystolic',
BLOOD_PRESSURE_DIASTOLIC = 'HKQuantityTypeIdentifierBloodPressureDiastolic',
BLOOD_OXYGEN = 'HKQuantityTypeIdentifierOxygenSaturation',
// 呼吸
RESPIRATORY_RATE = 'HKQuantityTypeIdentifierRespiratoryRate',
// 活动与运动
STEPS = 'HKQuantityTypeIdentifierStepCount',
DISTANCE_WALKING_RUNNING = 'HKQuantityTypeIdentifierDistanceWalkingRunning',
DISTANCE_CYCLING = 'HKQuantityTypeIdentifierDistanceCycling',
DISTANCE_SWIMMING = 'HKQuantityTypeIdentifierDistanceSwimming',
FLIGHTS_CLIMBED = 'HKQuantityTypeIdentifierFlightsClimbed',
// 能量消耗
ACTIVE_ENERGY_BURNED = 'HKQuantityTypeIdentifierActiveEnergyBurned',
BASAL_ENERGY_BURNED = 'HKQuantityTypeIdentifierBasalEnergyBurned',
// 营养
DIETARY_ENERGY = 'HKQuantityTypeIdentifierDietaryEnergyConsumed',
DIETARY_CARBOHYDRATES = 'HKQuantityTypeIdentifierDietaryCarbohydrates',
DIETARY_PROTEIN = 'HKQuantityTypeIdentifierDietaryProtein',
DIETARY_FAT_TOTAL = 'HKQuantityTypeIdentifierDietaryFatTotal',
DIETARY_FIBER = 'HKQuantityTypeIdentifierDietaryFiber',
DIETARY_SUGAR = 'HKQuantityTypeIdentifierDietarySugar',
DIETARY_SODIUM = 'HKQuantityTypeIdentifierDietarySodium',
WATER = 'HKQuantityTypeIdentifierDietaryWater',
CAFFEINE = 'HKQuantityTypeIdentifierDietaryCaffeine',
// 睡眠
SLEEP_ANALYSIS = 'HKCategoryTypeIdentifierSleepAnalysis',
// 体能训练
WORKOUT = 'HKWorkoutTypeIdentifier',
}
/**
* HealthKit 单位枚举
*/
export enum HKUnit {
// 长度
METER = 'm',
CENTIMETER = 'cm',
FOOT = 'ft',
INCH = 'in',
// 质量
KILOGRAM = 'kg',
GRAM = 'g',
POUND = 'lb',
OUNCE = 'oz',
// 能量
KILOCALORIE = 'kcal',
JOULE = 'J',
// 时间
SECOND = 's',
MINUTE = 'min',
HOUR = 'hr',
// 温度
CELSIUS = 'degC',
FAHRENHEIT = 'degF',
// 压力
MILLIMETER_OF_MERCURY = 'mmHg',
// 百分比
PERCENT = '%',
// 体积
LITER = 'L',
MILLILITER = 'mL',
// 浓度
GRAM_PER_DECILITER = 'g/dL',
MILLIGRAM_PER_DECILITER = 'mg/dL',
}
/**
* 数据读写权限配置
*/
export interface HealthKitPermissions {
read: HKQuantityType[];
write: HKQuantityType[];
}
Code collapsed
核心 API 封装
HealthKit 服务类
code
// lib/healthKit/HealthKitService.ts
import { NativeModules, Platform } from 'react-native';
import { HKQuantityType, HKUnit, HealthKitPermissions } from './types';
import { requestPermissions, openHealthKitSettings } from './permissions';
const { HealthKit } = NativeModules;
/**
* HealthKit 数据点接口
*/
export interface HealthDataPoint {
value: number;
unit: HKUnit;
timestamp: Date;
metadata?: Record<string, any>;
}
/**
* 健康数据汇总
*/
export interface HealthDataSummary {
total: number;
average: number;
minimum: number;
maximum: number;
unit: HKUnit;
}
/**
* 睡眠分析数据
*/
export interface SleepAnalysis {
startDate: Date;
endDate: Date;
value: 'IN_BED' | 'SLEEPING' | 'ASLEEP';
}
/**
* HealthKit 服务类
*/
export class HealthKitService {
private isInitialized = false;
/**
* 初始化 HealthKit
*/
async initialize(permissions: HealthKitPermissions): Promise<boolean> {
if (Platform.OS !== 'ios') {
console.warn('HealthKit is only available on iOS');
return false;
}
try {
const granted = await requestPermissions(permissions);
if (!granted) {
// 用户拒绝了部分或全部权限
const shouldOpenSettings = await this.promptUserForPermissions();
if (shouldOpenSettings) {
openHealthKitSettings();
}
return false;
}
this.isInitialized = true;
return true;
} catch (error) {
console.error('HealthKit initialization failed:', error);
return false;
}
}
/**
* 检查是否已授权
*/
async isAuthorized(type: HKQuantityType): Promise<boolean> {
try {
const authorizationStatus = await HealthKit.getAuthStatus?.(type);
return authorizationStatus === 'authorized';
} catch {
return false;
}
}
/**
* 读取最新数据点
*/
async getLatestDataPoint(
type: HKQuantityType,
unit: HKUnit
): Promise<HealthDataPoint | null> {
if (!this.isInitialized) {
throw new Error('HealthKit not initialized');
}
try {
const result = await HealthKit.getLatestQuantity?.(type, unit);
return result;
} catch (error) {
console.error('Failed to get latest data point:', error);
return null;
}
}
/**
* 读取指定日期范围的数据
*/
async getDataBetweenDates(
type: HKQuantityType,
unit: HKUnit,
startDate: Date,
endDate: Date
): Promise<HealthDataPoint[]> {
if (!this.isInitialized) {
throw new Error('HealthKit not initialized');
}
try {
const results = await HealthKit.getQuantitySamples?.(
type,
unit,
startDate.getTime(),
endDate.getTime()
);
return results || [];
} catch (error) {
console.error('Failed to get data between dates:', error);
return [];
}
}
/**
* 读取数据汇总
*/
async getDataSummary(
type: HKQuantityType,
unit: HKUnit,
startDate: Date,
endDate: Date
): Promise<HealthDataSummary | null> {
if (!this.isInitialized) {
throw new Error('HealthKit not initialized');
}
try {
const dataPoints = await this.getDataBetweenDates(
type,
unit,
startDate,
endDate
);
if (dataPoints.length === 0) {
return null;
}
const values = dataPoints.map(p => p.value);
return {
total: values.reduce((sum, v) => sum + v, 0),
average: values.reduce((sum, v) => sum + v, 0) / values.length,
minimum: Math.min(...values),
maximum: Math.max(...values),
unit
};
} catch (error) {
console.error('Failed to get data summary:', error);
return null;
}
}
/**
* 写入数据点
*/
async writeDataPoint(
type: HKQuantityType,
value: number,
unit: HKUnit,
timestamp: Date = new Date(),
metadata?: Record<string, any>
): Promise<boolean> {
if (!this.isInitialized) {
throw new Error('HealthKit not initialized');
}
try {
await HealthKit.writeQuantity?.(type, value, unit, timestamp.getTime(), metadata);
return true;
} catch (error) {
console.error('Failed to write data point:', error);
return false;
}
}
/**
* 批量写入数据点
*/
async writeDataPoints(
type: HKQuantityType,
dataPoints: Array<{ value: number; unit: HKUnit; timestamp: Date; metadata?: Record<string, any> }>
): Promise<{ success: number; failed: number }> {
let success = 0;
let failed = 0;
for (const point of dataPoints) {
const result = await this.writeDataPoint(
type,
point.value,
point.unit,
point.timestamp,
point.metadata
);
if (result) {
success++;
} else {
failed++;
}
}
return { success, failed };
}
/**
* 读取睡眠分析数据
*/
async getSleepAnalysis(
startDate: Date,
endDate: Date
): Promise<SleepAnalysis[]> {
if (!this.isInitialized) {
throw new Error('HealthKit not initialized');
}
try {
const results = await HealthKit.getSleepSamples?.(
startDate.getTime(),
endDate.getTime()
);
return results || [];
} catch (error) {
console.error('Failed to get sleep analysis:', error);
return [];
}
}
/**
* 写入睡眠分析数据
*/
async writeSleepAnalysis(
startDate: Date,
endDate: Date,
value: 'IN_BED' | 'SLEEPING' | 'ASLEEP'
): Promise<boolean> {
if (!this.isInitialized) {
throw new Error('HealthKit not initialized');
}
try {
await HealthKit.writeSleepSample?.(
startDate.getTime(),
endDate.getTime(),
value
);
return true;
} catch (error) {
console.error('Failed to write sleep analysis:', error);
return false;
}
}
/**
* 提示用户开启权限
*/
private async promptUserForPermissions(): Promise<boolean> {
// 这里可以显示一个自定义对话框
return new Promise((resolve) => {
// 实际应用中应使用 Alert 或自定义组件
resolve(true);
});
}
}
// 导出单例
export const healthKitService = new HealthKitService();
Code collapsed
React Hooks 集成
使用 HealthKit 的 Hook
code
// hooks/useHealthKit.ts
import { useState, useEffect, useCallback } from 'react';
import { healthKitService } from '@/lib/healthKit/HealthKitService';
import {
HKQuantityType,
HKUnit,
HealthDataPoint,
HealthDataSummary
} from '@/lib/healthKit/types';
export interface UseHealthKitOptions {
autoInitialize?: boolean;
permissions?: {
read: HKQuantityType[];
write: HKQuantityType[];
};
}
export function useHealthKit(options: UseHealthKitOptions = {}) {
const {
autoInitialize = true,
permissions = {
read: [
HKQuantityType.STEPS,
HKQuantityType.DISTANCE_WALKING_RUNNING,
HKQuantityType.ACTIVE_ENERGY_BURNED,
HKQuantityType.HEART_RATE,
HKQuantityType.SLEEP_ANALYSIS,
],
write: [
HKQuantityType.STEPS,
HKQuantityType.DISTANCE_WALKING_RUNNING,
HKQuantityType.ACTIVE_ENERGY_BURNED,
HKQuantityType.SLEEP_ANALYSIS,
],
},
} = options;
const [isAvailable, setIsAvailable] = useState(false);
const [isAuthorized, setIsAuthorized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// 初始化 HealthKit
useEffect(() => {
if (autoInitialize) {
initialize();
}
}, []);
const initialize = async () => {
setIsLoading(true);
const success = await healthKitService.initialize(permissions);
setIsAvailable(success);
setIsAuthorized(success);
setIsLoading(false);
};
// 读取今日步数
const getTodaySteps = useCallback(async (): Promise<number | null> => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const now = new Date();
const summary = await healthKitService.getDataSummary(
HKQuantityType.STEPS,
HKUnit.COUNT,
today,
now
);
return summary?.total ?? null;
}, []);
// 读取心率数据
const getHeartRate = useCallback(async (
hours: number = 24
): Promise<HealthDataPoint[]> => {
const endDate = new Date();
const startDate = new Date(endDate.getTime() - hours * 60 * 60 * 1000);
return await healthKitService.getDataBetweenDates(
HKQuantityType.HEART_RATE,
HKUnit.COUNT_PER_MINUTE,
startDate,
endDate
);
}, []);
// 写入步数
const writeSteps = useCallback(async (
value: number,
timestamp: Date = new Date()
): Promise<boolean> => {
return await healthKitService.writeDataPoint(
HKQuantityType.STEPS,
value,
HKUnit.COUNT,
timestamp,
{ wasUserEntered: true }
);
}, []);
// 写入锻炼数据
const writeWorkout = useCallback(async (
type: 'walking' | 'running' | 'cycling',
startDate: Date,
endDate: Date,
energyBurned?: number,
distance?: number
): Promise<boolean> => {
// HealthKit 有专门的工作类型,这里简化处理
// 实际应用中应使用 HKWorkout
return await healthKitService.writeDataPoint(
HKQuantityType.ACTIVE_ENERGY_BURNED,
energyBurned || 0,
HKUnit.KILOCALORIE,
endDate
);
}, []);
return {
isAvailable,
isAuthorized,
isLoading,
initialize,
getTodaySteps,
getHeartRate,
writeSteps,
writeWorkout,
};
}
Code collapsed
读取健康数据的 Hook
code
// hooks/useHealthData.ts
import { useState, useEffect } from 'react';
import { useHealthKit } from './useHealthKit';
import { HKQuantityType, HKUnit } from '@/lib/healthKit/types';
export function useHealthData() {
const { isAvailable, getTodaySteps, getHeartRate } = useHealthKit();
const [steps, setSteps] = useState<number | null>(null);
const [heartRate, setHeartRate] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!isAvailable) return;
const fetchHealthData = async () => {
setLoading(true);
try {
const [todaySteps, hrData] = await Promise.all([
getTodaySteps(),
getHeartRate(1), // 最近1小时
]);
setSteps(todaySteps);
if (hrData.length > 0) {
// 取最新心率
setHeartRate(hrData[hrData.length - 1].value);
}
} catch (error) {
console.error('Failed to fetch health data:', error);
} finally {
setLoading(false);
}
};
fetchHealthData();
// 每分钟更新一次
const interval = setInterval(fetchHealthData, 60000);
return () => clearInterval(interval);
}, [isAvailable, getTodaySteps, getHeartRate]);
return {
steps,
heartRate,
loading,
isAvailable,
};
}
Code collapsed
UI 组件示例
健康数据卡片
code
// components/health/HealthDataCard.tsx
import React from 'react';
import { useHealthData } from '@/hooks/useHealthData';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { FootprintsIcon, HeartIcon } from 'lucide-react';
export const HealthDataCard: React.FC = () => {
const { steps, heartRate, loading, isAvailable } = useHealthData();
if (!isAvailable) {
return (
<Card>
<CardContent className="p-6">
<p className="text-center text-gray-500">
HealthKit 不可用或未授权
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle>今日健康数据</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<FootprintsIcon className="w-6 h-6 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-500">步数</p>
{loading ? (
<p className="text-xl font-semibold">加载中...</p>
) : (
<p className="text-2xl font-bold">
{steps?.toLocaleString() || 0}
</p>
)}
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2 bg-red-100 rounded-lg">
<HeartIcon className="w-6 h-6 text-red-600" />
</div>
<div>
<p className="text-sm text-gray-500">心率</p>
{loading ? (
<p className="text-xl font-semibold">加载中...</p>
) : (
<p className="text-2xl font-bold">
{heartRate ? `${heartRate} bpm` : '--'}
</p>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
};
Code collapsed
数据同步策略
code
// lib/healthKit/sync.ts
import { healthKitService } from './HealthKitService';
import { HKQuantityType, HKUnit } from './types';
export class HealthKitSyncService {
/**
* 同步步数数据到本地存储
*/
static async syncStepsToLocalStorage(days: number = 7): Promise<void> {
const endDate = new Date();
const startDate = new Date(endDate);
startDate.setDate(startDate.getDate() - days);
const dataPoints = await healthKitService.getDataBetweenDates(
HKQuantityType.STEPS,
HKUnit.COUNT,
startDate,
endDate
);
// 按日期分组
const dailySteps = new Map<string, number>();
for (const point of dataPoints) {
const date = new Date(point.timestamp);
const dateKey = date.toISOString().split('T')[0];
dailySteps.set(dateKey, (dailySteps.get(dateKey) || 0) + point.value);
}
// 存储到本地
const syncData = Object.fromEntries(dailySteps);
await AsyncStorage.setItem('healthkit-steps-sync', JSON.stringify(syncData));
}
/**
* 从本地存储读取同步的步数数据
*/
static async getSyncedSteps(): Promise<Record<string, number>> {
const data = await AsyncStorage.getItem('healthkit-steps-sync');
return data ? JSON.parse(data) : {};
}
/**
* 定期同步(后台任务)
*/
static setupPeriodicSync(intervalMs: number = 3600000): void {
setInterval(() => {
this.syncStepsToLocalStorage();
}, intervalMs);
}
}
Code collapsed