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の意味が違うので気をつけましょう。
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
についてのみ説明します。
-
Data Types
-
HKCharacteristicType
- 時間とともに変化することのない情報
- 血液型や性別など
- Read専用
-
HKQuantityType
- 量で表す情報
- 各種栄養情報や歩数、血圧など
-
HKCategoryType
- 数種類の状態を表す情報
- 睡眠や生理情報など
-
HKCorrelationType
- 関連する複数のData Typeの集合を表す情報
- 食べ物(栄養素の組み合わせ)
- 血圧(拡張期と収縮期の組み合わせ)
- Others
-
HKCharacteristicType
HKQuantitySample
HKQuantitySample
は HKQuantityType
のカテゴリオブジェクトを実際に書込んだり読込んだりする時に利用するオブジェクトです。
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型の治療に使われるインスリンをいつ
、どのくらいの量
をなんのために
摂取したかを表す情報です。
このなんのため
にというのが無いと、書込時にクラッシュします。
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
HKCategorySample
は HKCategoryType
のカテゴリオブジェクトを実際に書込んだり読込んだりする時に利用するオブジェクトです。
HKCategorySample
の値は HKCategoryValueXXX
というような名前で定義されています。
使える値は利用するHKCategoryTypeIdentifier
のドキュメントに書いてあります。
下記コードのSleepAnalysis
の場合はドキュメントのDiscussionに
These samples use values from the HKCategoryValueSleepAnalysis enum.
と書いてありますのでそれを利用します。
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
を、食品では栄養素を組み合わせて使います。
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
の組み合わせであるため、これをアクセス権限要求の際には使えず、クラッシュします。
よって、以下のコードはクラッシュします。
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 }
最後に
もしクラッシュしてもログを見ればクラッシュした原因が分かるので、ドキュメントを読んで何が入るのか?を把握して対処しましょう!