はじめに
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-healthkit
のgithubレポジトリの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
にチェックを入れる)。
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.framework
とHealthKitUI.framework
を選択。project.pbxproj
に設定が反映される。
これで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に関して新しい知見があれば追記していきます。