Edited at
iOSDay 16

HealthKitで気をつけるポイント

iOS Advent Calendarの16日目を担当する、あすけんの@sato-shinです。

今日はHealthKitの気をつけるポイントについてご紹介します。


はじめに

みなさん、ヘルスケア App は利用していますか?食事や運動、バイタルデータを記録・閲覧できるハートアイコンのあれです。

Safariなどと同様にiPhoneにプリインされ、削除することのできないアプリなので、iPhoneユーザなら目にしたことのあると思います。

このアプリで管理しているデータを読込・書込を行うのがHealthKitです。

弊社では、ヘルスケアサービスである「あすけん」を開発しているので、HealthKitを利用することが多々あります。

この記事では私がHealthKitを利用する機能を作る上で、気をつけるポイントを紹介します。


HealthKitの利用を開始する

Setting Up HealthKitに詳しく書いていますので、そちらも参照ください。

HealthKitにはカテゴリ(水分摂取量、身長、体重、歩数...)ごとにRead権限とWrite権限を指定します。


  • Read権限とWrite権限の違い


    • Read権限のある項目は他のアプリの書込情報も読み取ることができる

    • Write権限のある項目は書込みと、自分のアプリが書込んだ情報の削除・閲覧ができる




Capabilityの設定

HealthKitの利用にはCameraなどを利用する場合と同じく、CapabilityをONにします。


plistに利用する理由を記載する

これもCameraなどを利用する場合と同じく、plistファイルになぜ利用するのか?をユーザ向けの文章で記載します。

不適切な説明はリジェクト対象となります。

<key>NSHealthShareUsageDescription</key>

<string>...Readする理由...</string>
<key>NSHealthUpdateUsageDescription</key>
<string>...Writeする理由...</string>


権限のリクエストを行う

次に実際にアプリで利用するカテゴリのRead/Write権限を要求するには以下のようにします。

plistファイルでは Read権限=Share / Write権限=Update という名称を利用していますが、

コードでは Read権限=Read / Write権限=Share とShareの意味が違うので気をつけましょう。


水摂取量と歩数のread/write権限を要求する

let store = HKHealthStore()

let types: Set<HKSampleType> = [
HKSampleType.quantityType(forIdentifier: .dietaryWater)!,
HKSqmpleType.quantityType(forIdentifier: .stepCount)!
]
store.requestAuthorization(toShare: types, read: types) { success, error in
... Error Handling など ...
}


クラッシュする権限要求

書込不可のカテゴリや、HKCorrelationType に対して、書込権限リクエストを送るとクラッシュするので注意です。


  • Apple Watchからの書込専用カテゴリ


    • HKQuantityTypeIdentifier.appleExerciseTime

    • HKCategoryTypeIdentifier.appleStandHour



  • Nike Fuelデバイスからの書込専用カテゴリ


    • HKQuantityTypeIdentifier.nikeFuel




書込不可のカテゴリに対して、書込権限リクエストを送るとクラッシュ

let store = HKHealthStore()

let sharedTypes: Set<HKSampleType> = [
HKSampleType.quantityType(forIdentifier: .appleExerciseTime)!
]
store.requestAuthorization(toShare: sharedTypes, read: nil) { _, _ in }

ちなみにHKSampleTypeを継承していないカテゴリは、Write権限を要求するコードはコンパイルエラーになるので安心です。


情報を操作する


DataTypes

HealthKitのデータを取り扱うには、まずはData Typesについて知っておかなければなりません。

Data TypesはHealthKitで管理できる情報の種類です。

今回はHKCharacteristicType, HKQuantityType, HKCategoryType, HKCorrelationTypeについてのみ説明します。


HKQuantitySample

HKQuantitySampleHKQuantityType のカテゴリオブジェクトを実際に書込んだり読込んだりする時に利用するオブジェクトです。


歩数情報を書き込む

let now = Date()

let quantity = HKQuantity(unit: .count(), doubleValue: 10) // 10歩歩いた
let objects: HKObject = HKQuantitySample(
type: HKSampleType.quantityType(forIdentifier: .stepCount)!,
quantity: quantity, start: now, end: now.addingTimeInterval(100))
store.save(objects) { success, error in }


Metadataが必須な場合があり、無いとクラッシュする

HKQuantitySampleの一つである、InsulinDeliveryでは特定のMetadataが無いとクラッシュします。

InsulinDeliveryは名前の通り、主に糖尿病1型の治療に使われるインスリンをいつどのくらいの量なんのために摂取したかを表す情報です。

このなんのためにというのが無いと、書込時にクラッシュします。


InsulinDeliveryはInsulindeliveryReasonが無いとクラッシュ

let now = Date()

let quantity = HKQuantity(unit: .internationalUnit(), doubleValue: 1)
let reason = [HKMetadataKeyInsulinDeliveryReason: HKInsulinDeliveryReason.basal.rawValue] // このメタデータが無いとクラッシュ
let object = HKQuantitySample(type: HKSampleType.quantityType(forIdentifier: .insulinDelivery)!,
quantity: quantity, start: now, end: now, metadata: reason)
store.save(object) { _, _ in }


HKQuantity で単位を間違うとクラッシュする

HKQuantity オブジェクト単位情報(HKUnit)を間違えるとクラッシュします。

以下の例では、10 gram 歩いたという、ありえない情報を書込もうとしています。


単位を間違えるとクラッシュ

let now = Date()

let quantity = HKQuantity(unit: .gram(), doubleValue: 10) // 10 グラム歩いた???
let objects: HKObject = HKQuantitySample(
type: HKSampleType.quantityType(forIdentifier: .stepCount)!,
quantity: quantity, start: now, end: now.addingTimeInterval(100))
store.save(objects) { _, _ in }


単位を組み合わせる

HKQuantityType の中には複数の単位によって、単位を定義しなければならないモノもあります

例えば、最大酸素摂取量(mL/kg*min)や血糖値(mg/dL or millimole/L)では単位を組み合わせることが必須となります。

HKUnit には unitMultiplied(by:), unitDivided(by:), unitRaised(toPower:) といった関数があるので、これを利用して単位を作りましょう。


最大酸素摂取量の単位を作る

var vo2MaxUnit = {

let denom = HKUnit.gramUnit(with: .kilo).unitMultiplied(by: .minute())
let numer = HKUnit.literUnit(with: .milli)
return numer.unitDivided(by: denom)
}


HKCategorySample

HKCategorySampleHKCategoryType のカテゴリオブジェクトを実際に書込んだり読込んだりする時に利用するオブジェクトです。

HKCategorySample の値は HKCategoryValueXXXというような名前で定義されています。

使える値は利用するHKCategoryTypeIdentifierのドキュメントに書いてあります。

下記コードのSleepAnalysis

の場合はドキュメントのDiscussionに


These samples use values from the HKCategoryValueSleepAnalysis enum.


と書いてありますのでそれを利用します。


6時間の間前から現在まで、ベッドに横たわっていたという情報を書き込む

let now = Date()

let sleepState = HKCategoryValueSleepAnalysis.inBed
let object = HKCategorySample(
type: HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!,
value: sleepState.rawValue,
start: now.addingTimeInterval(-60*60*6), end: now)
store.save(object) { _, _ in }


HKCorrelationSample

HKCorrelationSampleHKCorrelationType カテゴリオブジェクトを実際に書込んだり読み込んだ入りする時に利用するオブジェクトです。

HKCorrelationSampleHKObject の組み合わせを表現します。

現在は、血圧と食品の二つのみサポートしています。

血圧では bloodPressureSystolicbloodPressureDiastolicを、食品では栄養素を組み合わせて使います。


血圧情報を登録する

let now = Date()

let diastolic = HKQuantitySample(
type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!,
quantity: HKQuantity(unit: .millimeterOfMercury(), doubleValue: 80),
start: now, end: now)
let systolic = HKQuantitySample(
type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!,
quantity: HKQuantity(unit: .millimeterOfMercury(), doubleValue: 120),
start: now, end: now)
let object = HKCorrelation(type: HKSampleType.correlationType(forIdentifier: .bloodPressure)!,
start: now, end: now, objects: [diastolic, systolic])
store.save(object) { _, _ in }


アクセス権限要求はHKSampleTypeで行う

HKCorrelationTypeHKObjectの組み合わせであるため、これをアクセス権限要求の際には使えず、クラッシュします。

よって、以下のコードはクラッシュします。


血圧の組合せへのアクセス権限要求はクラッシュする

let store = HKHealthStore()

let types: Set<HKSampleType> = [
HKSampleType.correlationType(forIdentifier: .bloodPressure)!
]
store.requestAuthorization(toShare: types, read: types) { _, _ in }

これを回避するためには血圧を表す上限と下限のカテゴリに対してアクセス要求を行います。


血圧上限,下限のカテゴリに対してアクセス要求を行う

let store = HKHealthStore()

let types: Set<HKSampleType> = [
HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!,
HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!
]
store.requestAuthorization(toShare: types, read: types) { _, _ in }


最後に

もしクラッシュしてもログを見ればクラッシュした原因が分かるので、ドキュメントを読んで何が入るのか?を把握して対処しましょう!