康心伴Logo
康心伴WellAlly
移动开发

React Native 与 Apple HealthKit 集成完整指南

5 分钟阅读

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 能力:

  1. 在 Xcode 中打开项目
  2. 选择 Target → Signing & Capabilities
  3. 添加 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

参考资料

#

文章标签

react-native
apple-healthkit
健康数据
iOS开发
可穿戴设备

觉得这篇文章有帮助?

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