HealthKitで気をつけるポイント

iOS Advent Calendarの16日目を担当する、あすけんの@sato-shinです。
今日は`HealthKit`の気をつけるポイントについてご紹介します。

# はじめに
みなさん、[ヘルスケア](https://www.apple.com/jp/ios/health/) App は利用していますか？食事や運動、バイタルデータを記録・閲覧できるハートアイコンのあれです。
Safariなどと同様にiPhoneにプリインされ、削除することのできないアプリなので、iPhoneユーザなら目にしたことのあると思います。

このアプリで管理しているデータを読込・書込を行うのが`HealthKit`です。
弊社では、ヘルスケアサービスである「あすけん」を開発しているので、HealthKitを利用することが多々あります。
この記事では私がHealthKitを利用する機能を作る上で、気をつけるポイントを紹介します。

# HealthKitの利用を開始する
[Setting Up HealthKit](https://developer.apple.com/documentation/healthkit/setting_up_healthkit)に詳しく書いていますので、そちらも参照ください。

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

- Read権限とWrite権限の違い
    - Read権限のある項目は他のアプリの書込情報も読み取ることができる
    - Write権限のある項目は書込みと、自分のアプリが書込んだ情報の削除・閲覧ができる

## Capabilityの設定
HealthKitの利用にはCameraなどを利用する場合と同じく、CapabilityをONにします。
![スクリーンショット 2018-12-16 14.16.16.png](https://qiita-image-store.s3.amazonaws.com/0/46792/1f515faf-6cfe-ca51-0e59-a06fe8373684.png)

## plistに利用する理由を記載する
これもCameraなどを利用する場合と同じく、plistファイルに`なぜ利用するのか？`をユーザ向けの文章で記載します。
不適切な説明はリジェクト対象となります。

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

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

plistファイルでは `Read権限=Share` / `Write権限=Update` という名称を利用していますが、
コードでは `Read権限=Read` / `Write権限=Share` とShareの意味が違うので気をつけましょう。

```swift:水摂取量と歩数の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

```swift:書込不可のカテゴリに対して、書込権限リクエストを送るとクラッシュ
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`についてのみ説明します。

- [Data Types](https://developer.apple.com/documentation/healthkit/data_types)
    - [HKCharacteristicType](https://developer.apple.com/documentation/healthkit/hkcharacteristictype)
        - 時間とともに変化することのない情報
        - 血液型や性別など
        - Read専用
    - [HKQuantityType](https://developer.apple.com/documentation/healthkit/hkquantitytype)
        - 量で表す情報
        - 各種栄養情報や歩数、血圧など
    - [HKCategoryType](https://developer.apple.com/documentation/healthkit/hkcategorytype)
        - 数種類の状態を表す情報
        - 睡眠や生理情報など
    - [HKCorrelationType](https://developer.apple.com/documentation/healthkit/hkcorrelationtype)
        - 関連する複数のData Typeの集合を表す情報
        - 食べ物(栄養素の組み合わせ)
        - 血圧(拡張期と収縮期の組み合わせ)
    - Others
        - [HKActivitySummaryType](https://developer.apple.com/documentation/healthkit/hkactivitysummarytype)
        - [HKSeriesType](https://developer.apple.com/documentation/healthkit/hkseriestype)
        - [HKClinicalType](https://developer.apple.com/documentation/healthkit/hkclinicaltype)
        - [HKDocumentType](https://developer.apple.com/documentation/healthkit/hkdocumenttype)

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

```swift:歩数情報を書き込む
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型の治療に使われるインスリンを`いつ`、`どのくらいの量`を`なんのために`摂取したかを表す情報です。
この`なんのため`にというのが無いと、書込時にクラッシュします。

```swift: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 歩いた`という、ありえない情報を書込もうとしています。

```swift:単位を間違えるとクラッシュ
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:)` といった関数があるので、これを利用して単位を作りましょう。

```swift:最大酸素摂取量の単位を作る
var vo2MaxUnit = {
    let denom = HKUnit.gramUnit(with: .kilo).unitMultiplied(by: .minute())
    let numer = HKUnit.literUnit(with: .milli)
    return numer.unitDivided(by: denom)
}
```

## HKCategorySample
`HKCategorySample` は `HKCategoryType` のカテゴリオブジェクトを実際に書込んだり読込んだりする時に利用するオブジェクトです。
`HKCategorySample` の値は `HKCategoryValueXXX`というような名前で定義されています。
使える値は利用する`HKCategoryTypeIdentifier`のドキュメントに書いてあります。

下記コードの[SleepAnalysis](https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/1615425-sleepanalysis)
の場合はドキュメントのDiscussionに

> These samples use values from the HKCategoryValueSleepAnalysis enum.

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

```swift: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
`HKCorrelationSample` は `HKCorrelationType` カテゴリオブジェクトを実際に書込んだり読み込んだ入りする時に利用するオブジェクトです。
`HKCorrelationSample` は `HKObject` の組み合わせを表現します。
現在は、血圧と食品の二つのみサポートしています。

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

```swift:血圧情報を登録する
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で行う
`HKCorrelationType` は `HKObject`の組み合わせであるため、これをアクセス権限要求の際には使えず、クラッシュします。
よって、以下のコードはクラッシュします。

```swift:血圧の組合せへのアクセス権限要求はクラッシュする
let store = HKHealthStore()
let types: Set<HKSampleType> = [
    HKSampleType.correlationType(forIdentifier: .bloodPressure)!
]
store.requestAuthorization(toShare: types, read: types) { _, _ in }
```

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

```swift:血圧上限,下限のカテゴリに対してアクセス要求を行う
let store = HKHealthStore()
let types: Set<HKSampleType> = [
    HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!,
    HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!
]
store.requestAuthorization(toShare: types, read: types) { _, _ in }
```

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