康心伴Logo
康心伴WellAlly
Health

React Native He...?看这篇就够了 | WellAlly康心伴

5 分钟阅读

React Native HealthKit步数睡眠数据集成完整指南

概述

Apple HealthKit是iOS设备上最全面的健康数据平台,可访问:

  • 步数:每日行走步数
  • 睡眠分析:睡眠时长和阶段
  • 心率:静息心率和HRV
  • 能量消耗:活动卡路里
  • 距离:行走/跑步距离

本教程将教你如何使用React Native集成HealthKit。


技术方案

集成方式

方案优点缺点
react-native-healthreact-native-health功能完整,支持读写配置复杂
expo-sensorsExpo官方简单易用功能有限
原生模块自定义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

关键要点

  1. 权限请求必须在Info.plist声明:iOS要求
  2. iOS 16+有新睡眠API:需适配
  3. 数据量限制:最多读取最近30天
  4. 后台观察需要权限:设置后台模式
  5. 数据写入需谨慎:避免重复写入

常见问题

Android怎么办?

使用Google Fit API:

code
import { GoogleFit } from 'react-native-google-fit';
Code collapsed

数据不准确?

可能原因:

  1. 用户未穿戴设备
  2. 设备未同步
  3. 不同应用写入冲突

权限被拒绝?

处理策略:

  1. 引导用户到设置页面
  2. 说明数据用途
  3. 提供部分功能降级

参考资料

  • Apple HealthKit文档
  • react-native-health文档
  • 健康数据隐私指南
  • 后台模式配置

发布日期:2026年3月8日 最后更新:2026年3月8日

免责声明: 本内容仅供教育参考,不能替代专业医疗建议。请咨询医生获取个性化诊断和治疗方案。

#

文章标签

React Native
HealthKit
健康数据
Apple Health
步数睡眠

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅