9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React NativeでHealthKitとHealth Connectと連携する

Last updated at Posted at 2024-06-19

はじめに

iOSにはHealthKit、AndroidにはHealth Connectと呼ばれるユーザーの睡眠時間や歩数等のヘルスデータを管理する仕組みが組み込まれています(それぞれiOS 8、 Android 14からで、Android 13以前のAndroidユーザーは手動でHealth Connectをインストールすることで利用可能)。また、iOSにはヘルスケア、AndroidにはGoogle Fitというデフォルトでインストールされているアプリがあり、これらを使うことでユーザーはHealthKit、Health Connectに蓄積された自分のヘルスデータの確認が可能となっています。

今回はReact Nativeを使ったサードパーティ製アプリでiOS、Androidそれぞれのヘルスデータを取得するために必要な設定や簡単なサンプルコードをご紹介します。iOSとAndroidでそれぞれ違ったデータ構造や内部的なデータベースへのクエリを扱うため、利用するライブラリはそれぞれ異なります。iOSにはreact-native-healthkit、androidにはreact-native-health-connectというライブラリがあります。

iOS向けにreact-native-healthというライブラリもあり、こちらの方が執筆時にはスター数が多かったのですが、もう一年ほど開発が活発にされていないように見受けられました。READMEには以下のようにSwiftで実装したバージョンに変わるという表記があるのですが、今のところこちらのリリースに関する情報はなさそうです。

We're thrilled to share that we're in the midst of creating a significant update for this library. The upcoming version will be crafted using Swift (bye-bye, Objective-C! 👋) and will showcase a fresh new interface.

Expoを使用されている場合は、以前こちらの記事で触れた通り、Expo prebuild等を利用すればExpoをejectすることなく、問題なく動作確認しながら進めることができます。

react-native-health-connect に関してはこちらの公式のドキュメントサイトがあります。内部的なネイティブ実装をより理解するには以下のそれぞれのドキュメントが参考になりました。

HealthKit (iOS)

基本はreact-native-healthkitgithubレポジトリのREADMEを見て進めましたが、私の場合、Xcode上で設定可能なFrameworks, Libraries, and Embedded Contentの設定が不足していました(エラーはfailed to update bundle ID capabilities: unknown entitlement key: com.apple.developer.healthkit.access (exit code: 1)といった内容)。また後半のステップの bridging header を追加するところも少しつまづきました。

Discordのチャンネルもあるようなのでもし困ったらそこで聞いてみるのも良いかもしれません。

セットアップ

1. react-native-healthkitをインストール

npm i @kingstinct/react-native-healthkit

2. info.plistに設定を追加

info.plist に以下を追加。もしくはiosディレクトリをXcodeで開き左のサイドバーからプロジェクトを選択し、InfoタブにあるCustom iOS Target Properties から追加することも可能(ユーザーの臨床記録にアクセスする場合は、Privacy - Health Records Usage Descriptionを選択)。

<!-- info.plist -->

<key>NSHealthShareUsageDescription</key>
<string>Read Health Data</string>
<key>NSHealthUpdateUsageDescription</key>
<string>Write Health Data</string>

3. HealthKit Capabilityの追加

iosディレクトリをXcodeで開き左のサイドバーからプロジェクトを選択し、Signing & Capabilitiesタブを選択、左上の+ Capabilityをクリックする。選択メニューが表示されるのでスクロールしてHealthKitをダブルクリック(ユーザーの臨床記録にアクセスする場合は、その後に画面下部のClinical Health Recordsにチェックを入れる)。

1.png

HealthKit Capabilityの入ったentitlements ファイルが作成される

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.developer.healthkit</key>
	<true/>
	<key>com.apple.developer.healthkit.access</key>
	<array/>
</dict>
</plist>

4. bridging headerの設定

XcodeのBuild Settingsにbridging headerの設定が見当たらなかったが、READMEに記載されたこちらのリンクの通り、ファイルをXcode上で新規作成することでbridging headerが無事設定された。その後プロジェクトで不要と思われるファイルのみ削除。

5. Expo向けの設定

app.json

{
  "expo": {
    "plugins": ["@kingstinct/react-native-healthkit"]
  }
}

6. その他不足していたと思われる設定

XcodeのGeneralタブを選択し、Frameworks, Libraries, and Embedded ContentのセクションからHealthKit.frameworkHealthKitUI.frameworkを選択。project.pbxprojに設定が反映される。

2.png

これでHealthKit向けの初期設定は完了です。

サンプル

歩数をHealthKitから取得し表示するサンプルです。

import { useEffect, useState } from "react";
import { View, Text, Button, Alert } from "react-native";
import HealthKit, {
  queryStatisticsForQuantity,
  HKQuantityTypeIdentifier,
  HKStatisticsOptions,
} from "@kingstinct/react-native-healthkit";

const App = () => {
  const [steps, setSteps] = useState<number | null>(null);

  useEffect(() => {
    const getPermission = async () => {
      try {
        // HealthKitが利用可能か確認
        const healthKitAvailable = await HealthKit.isHealthDataAvailable();

        if (!healthKitAvailable) {
          throw new Error("HealthKit is not avaliable");
        }

        // パーミッションの取得
        await HealthKit.requestAuthorization([
          HKQuantityTypeIdentifier.stepCount,
        ]);
      } catch (error) {
        console.error(error);
        Alert.alert("エラーが発生しました。");
      }
    };

    getPermission();
  }, []);

  const fetchSteps = async () => {
    try {
      const now = new Date();
      const startOfDay = new Date(
        now.getFullYear(),
        now.getMonth(),
        now.getDate()
      );

      // 歩数の取得
      const response = await queryStatisticsForQuantity(
        HKQuantityTypeIdentifier.stepCount,
        [HKStatisticsOptions.cumulativeSum],
        startOfDay,
        now
      );

      const totalSteps = response.sumQuantity?.quantity || 0;
      setSteps(totalSteps);
    } catch (error) {
      console.error(error);
      Alert.alert("エラーが発生しました。");
    }
  };

  return (
    <View>
      <Text>今日の歩数: {steps !== null ? steps : "歩数を取得中..."}</Text>
      <Button title="Refresh" onPress={fetchSteps} />
    </View>
  );
};

export default App;

Health Connect (Android)

react-native-health-connectの初期設定は公式のドキュメントサイトがあるのでそちらに沿って進めれば基本大丈夫だと思いますが(以下セットアップの1〜6)、私の場合いくつか追加の設定が必要でした(セットアップの7)。

セットアップ

1. react-native-health-connectをインストール

npm i react-native-health-connect

2. MainActivity.ktに設定を追加

以下ドキュメントの通りそのまま。

// 下記importを追加
+ import android.os.Bundle
+ import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate

// MainActivityにonCreateを追加
+ override fun onCreate(savedInstanceState: Bundle?) {
+   super.onCreate(savedInstanceState)
+   HealthConnectPermissionDelegate.setPermissionDelegate(this)
+ }

3. AndroidManifest.xmlにパーミッション設定を追加

android/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
+   <uses-permission android:name="android.permission.health.READ_STEPS"/>
+   <uses-permission android:name="android.permission.health.WRITE_STEPS"/>
</manifest>

4. PermissionRationaleActivity.ktファイルを追加

android/app/src/main/java/com/healthconnectexample/PermissionRationaleActivity.kt

package com.healthconnectexample

import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity

class PermissionsRationaleActivity: AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val webView = WebView(this)
    webView.webViewClient = object : WebViewClient() {
      override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        return false
      }
    }

    webView.loadUrl("https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started")

    setContentView(webView)
  }
}

5. AndroidManifest.xmlに以下のパーミッション設定を追加

android/src/main/AndroidManifest.xml

    <!-- For supported versions through Android 13, create an activity to show the rationale of Health Connect permissions once users click the privacy policy link. -->
    <activity
      android:name=".PermissionsRationaleActivity"
      android:exported="true">
      <intent-filter>
        <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
      </intent-filter>
    </activity>

    <!-- For versions starting Android 14, create an activity alias to show the rationale of Health Connect permissions once users click the privacy policy link. -->
    <activity-alias
      android:name="ViewPermissionUsageActivity"
      android:exported="true"
      android:targetActivity=".PermissionsRationaleActivity"
      android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
      <intent-filter>
        <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
        <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
      </intent-filter>
    </activity-alias>

6. Expo向けの設定

app.json

{
  "expo": {
    ...
    "plugins": [
      "react-native-health-connect",
      [
        "expo-build-properties",
        {
          "android": {
            "compileSdkVersion": 34,
            "targetSdkVersion": 34,
            "minSdkVersion": 26
          }
        }
      ]
    ],
    "android": {
      ...
      "permissions": [
        "android.permission.health.READ_STEPS",
        "android.permission.health.WRITE_STEPS",
        "android.permission.health.READ_ACTIVE_CALORIES_BURNED"
      ]
    }
  }
}

7. その他不足していたと思われる設定

私の場合は、ビルドが通らず以下の設定も追加しました。

android/app/build.gradle

+ implementation "androidx.health.connect:connect-client:1.0.0-alpha11"
...
+ implementation project(':react-native-health-connect')

android/app/src/main/AndroidManifest.xml

+ <queries>
+   <package android:name="com.google.android.apps.healthdata" />
+ </queries>

android/build.gradle

buildscript {
    ext {
        buildToolsVersion = "34.0.0"
        minSdkVersion = 26
        compileSdkVersion = 34
        targetSdkVersion = 34
        ...

packages/client/android/settings.gradle

include ':react-native-health-connect'
project(':react-native-health-connect').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-health-connect/android')

なんとかビルドが通りました。

サンプル

歩数をHealth Connectから取得し表示するサンプルです。

import { useEffect, useState } from "react";
import { View, Text, Button, Alert } from "react-native";
import {
  initialize,
  requestPermission,
  readRecords,
  getSdkStatus,
  SdkAvailabilityStatus,
} from "react-native-health-connect";

const delay = async (ms: number) => {
  return new Promise((resolve) => setTimeout(resolve, ms));
};

const App = () => {
  const [steps, setSteps] = useState<number | null>(null);

  useEffect(() => {
    const getPermission = async() => {
      try {
        // Health Connectがインストールされているかの確認
        const status = await getSdkStatus();
        if (status === SdkAvailabilityStatus.SDK_UNAVAILABLE) {
          Alert.alert("Health Connectインストールされていません。Google Play Storeからダウンロードしてください。");
          return;
        }
    
        if (status === SdkAvailabilityStatus.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
          Alert.alert("Health Connectプロバイダーの更新が必要です。");
          return;
        }
  
        // Health Connect Clientの初期化
        await initialize();
  
        // 私の場合、少しdelayが必要でした
        await delay(1000);
  
        // パーミッションの取得
        const granted = await requestPermission([
          { accessType: "read", recordType: "Steps" },
        ]);

        // パーミッションを取得できたので歩数を取得
        if (granted) {
          fetchSteps();
        }
      } catch (error) {
        console.error(error);
        Alert.alert("エラーが発生しました。");
      }
    };

    getPermission();
  }, []);

  const fetchSteps = async () => {
    try {
      const now = new Date();
      const currentTime = now.toISOString();
      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();

      // 歩数の取得
      const stepsData = await readRecords("Steps", {
        timeRangeFilter: {
          operator: "between",
          startTime: startOfDay,
          endTime: currentTime,
        },
      });

      const totalSteps = stepsData.reduce((sum, entry) => sum + entry.count, 0);
      setSteps(totalSteps);
    } catch (error) {
      console.error(error);
      Alert.alert("エラーが発生しました。");
    }
  };

  return (
    <View>
      <Text>今日の歩数: {steps !== null ? steps : "歩数を取得中..."}</Text>
      <Button title="Refresh" onPress={fetchSteps} />
    </View>
  );
};

export default App;

細かい話ですが、react-native-health-connectでは内部的な型がルートでexportされておらず、以下で対応可能でした。

import { Permission } from 'react-native-health-connect/src/types';

グラフ表示用のクエリ

グラフの表示等で、例えば30日間における1日ごとの歩数の合計を配列で取得したい場合HealthKitにはHKStatisticsCollectionQueryと呼ばれる強力なクエリがあるのですが、執筆時にはライブラリ側ではまだ実装がされていないようでした(恐らく時間の問題)。

そしてHealth Connectも同様にAggregateGroupByDurationRequestというリクエストオブジェクトがネイティブ実装側にあり、まだライブラリ側には実装されていないという、とても似た状況となっています。そのため現状だと例えば2ヶ月間の日別データを取得する際はヘルスデータの種類ごとに aggregateRecord を日別に計60回ずつ実行するような実装になってしまいます(HealthKitも同様)。明確な上限値はドキュメントには記載されていないですが、Rate Limitが存在しているため、膨大なリクエスト数になりそうな場合はリクエストのバッチをchunkに分割するなどの工夫が必要かもしれません。

(追記)こちらの件、メソッド追加のPRを上げて無事取り込んでもらえました。HealthKitの方はレビュー待ち。

別端末とのヘルスデータ共有

また、まだまだ理解が浅いところなのですが、iOSはHealthKitデータをiCloudに保存できるので恐らく別端末から容易に復元ができ、Android側では現状そういったことが難しいのでは?と思っています。

Googleの公式動画でも自前のサーバー経由でヘルスデータを連携することを言及しているのでやはり一手間かかりそうです。ただしウェアラブル端末とは動画で説明されている Wearable Data Layer API というAPIで連携できるかもしれません。

また、AndroidではHealth Connectからデータを取得する際にdataOriginFilterというフィルターを使って特定のアプリから取得したヘルスデータのみ取得、などのフィルタリングが可能そうです。

さいごに

基本的なコンセプトは同じなのですが、それぞれのヘルスデータにおいてHealthKitとHealthConnectで扱うデータ構造や取得するクエリが変わってくるため、最初慣れるまでに少し時間を要しました。

引き続きHealthKitとHealth Connectに関して新しい知見があれば追記していきます。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?