[iOS] Healthkitを使う前に知っておきたいハマりどころ

# Healthkitとは

iOSでは、iOSやサードパーティアプリから `ヘルスケア` にいろいろなヘルスケアデータが書き込まれます。
iOSアプリはヘルスケアデータをHealthkit経由で読み書きが可能で、またデータの更新を監視することができます。

それでは直近2年ぐらいで踏み抜いたHealthkit関連のTipsをどうぞ

# Healthkitのハマりどころ

## 1. iPadをサポートしてないアプリでもiPadで機能が動いてないとAppStore申請時にリジェクトされる

#### 原因
- 「ヘルスケア」はiPadに入ってないのでiPadのサポートを切ることが多い
- しかしAppleの人はiPadのiPhone互換モードで動作確認する

#### 解決策

↓のメソッドでヘルスケアが利用可能な端末か識別して制御しておく

```swift:HKHealthStore
class func isHealthDataAvailable() -> Bool
```

## 2. AppStoreの申請情報の書き方でリジェクトされる

ヘルスケアデータのデータソースとしてAppleのヘルスケア以外も対応してるアプリで↓のような文章を書くとAppStoreレビューでリジェクトされることがある。

```:リジェクトされる例
このアプリはFitbitから歩数データを連携できます！
```

#### 解決策

Appleのヘルスケアも対応していることを併記する

```:リジェクトされない例
このアプリはAppleのヘルスケアやFitbitから歩数データを連携できます！
```

## 3. パーミッション要求しても許可画面が表示されないことがある

↓のメソッドでヘルスケアのパーミッション要求をすると許可画面が表示されるはずが、

```swift:HKHealthStore
func requestAuthorization(toShare typesToShare: Set<HKSampleType>?, 
                     read typesToRead: Set<HKObjectType>?, 
               completion: @escaping (Bool, Error?) -> Void)
```

completion に↓のようなErrorが返ってきて表示されない。

```
Error Domain=com.apple.healthd.SQLite Code=1 "Transaction block failed without an error." UserInfo={NSLocalizedDescription=Transaction block failed without an error.}
``` 

#### 原因

- 数年前から[開発者フォーラム](https://developer.apple.com/forums/thread/80735) に投稿されているが明確な原因はわからない
- iPhoneSE第一世代/iOS13.2.1で実際に発生していた
- アプリのアンインストール/再インストールを繰り返しているとまれに発生する

#### 解決策

- 端末を再起動すると発生しなくなった（事象ベースで根本的に解決してませんすいません）
- パーミッション要求のエラーハンドリングでユーザに端末の再起動を促す

## 4. アクセス許可したあとにタップ操作ができなくなることがある

#### 原因

ヘルスケアのパーミッション要求し、ユーザが許可画面で許可/不許可を設定して画面がクローズした後も**Healthkitが表示した透明のViewを持つUIWindowが最前面に残り続ける**ことがある。

#### 解決策

アプリのメインWindowとHealthkitが表示したWindowの前面になるようWindowレベルを設定する。
詳しくは[こちら](https://qiita.com/dotrikun/items/08c2a431911baecd86fa)をどうぞ。

## 5. データの読み出しが不許可になっていることはわからない/データが微妙に取得できない

書き込みのパーミッションは↓で取得できるが、**読み出しの許可状態はわからない**

```swift:HKHealthStore
func authorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus
```

#### 理由

- [ドキュメント](https://developer.apple.com/documentation/healthkit/hkhealthstore/1614154-authorizationstatus) に書かれているとおり、機密性の高い健康情報の漏洩を防ぐためにアプリはユーザーがデータの読み出しを許可したかどうかを判断できないようになっている
- 取得処理を実行した場合はデータがないように見える
  - 自身のアプリが書き込んだデータは読み出しが不許可でも取得できる

## 6. データが取得できない

#### 原因

- 前述のとおり読み出しが拒否されてたら取れない
- 端末がロックされているときは暗号化されているためエラーになる
    - 以前、[アプリが終了している状態でもヘルスケアデータの更新をフックして処理を実行できる](https://qiita.com/dotrikun/items/73db477f8fb23f9d783b) という記事を書いた。こちらは**更新があったこと自体は検知できる**が、端末がロックされているときはデータ取得がエラーになるため工夫が必要
    - WidgetKitなどでも同じ

## 7. 取得したデータの値が異常なことがある

歩数など1日で歩けないような数値が入ってくることがある

#### 原因①

データを手入力できるアプリを経由してヘルスケアに書き込まれている

#### 解決策①

Appleの端末で自動計測されたデータだけに絞る（サードパーティアプリからヘルスケアに連携されたデータは含まない）

こんな感じで限定することができる

```swift:一定期間の日次歩数合計をApple端末の自動計測だけに絞って取得するサンプル
let healthStore = HKHealthStore()

let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let now = Date()
let start = now.addingTimeInterval(-1 * 60 * 60 * 24 * 30)
let end = now
let anchorDate = Calendar.current.startOfDay(for: now)
var intervalComponents = DateComponents()
intervalComponents.day = 1

let datePredicate = HKQuery.predicateForSamples(withStart: start, end: end)
let statisticsCollectionQuery = HKStatisticsCollectionQuery(quantityType: stepCountType,
                                        quantitySamplePredicate: datePredicate,
                                        options: [.cumulativeSum, .separateBySource],
                                        anchorDate: anchorDate,
                                        intervalComponents: intervalComponents)

statisticsCollectionQuery.initialResultsHandler = { query, results, error in
    guard let sources = results?.sources().filter({ $0.bundleIdentifier.lowercased().hasPrefix("com.apple.health") }) else {
        return
    }
    
    let sourcesPredicate = HKQuery.predicateForObjects(from: Set(sources))
    let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [datePredicate, sourcesPredicate])
    
    let statisticsCollectionQuery2 = HKStatisticsCollectionQuery(quantityType: stepCountType,
                                            quantitySamplePredicate: predicate,
                                            options: [.cumulativeSum, .separateBySource],
                                            anchorDate: anchorDate,
                                            intervalComponents: intervalComponents)
    
    statisticsCollectionQuery2.initialResultsHandler = { query, results, error in
        results?.statistics().forEach {
            print($0.sumQuantity()) // 日次の歩数合計
        }
        
    }
    
    healthStore.execute(statisticsCollectionQuery2)
}
healthStore.execute(statisticsCollectionQuery)
```
※`HKStatisticsCollectionQuery` でソースを絞るやり方はもっといい方法があるかもしれません

#### 原因②

ヘルスケアでデータが手入力されている

※ヘルスケアの入力画面
<img width="320" alt="sample.image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72194/50ba7763-77c8-2812-184f-62b581e92aad.png">

#### 解決策②

ヘルスケアに手入力されたデータはクエリで除外する

`HKSampleQuery` や `HKStatisticsQuery` などに↓を追加するとユーザが手入力したデータを除外できる

```swift
let predicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
```

## 8. iCloudバックアップ/iTunesバックアップからの復元時、ヘルスケアからアプリへの読み出し/書き込み許可はリセットされる

- UserDefaultsやKeychainなどのデータやプッシュ通知許可などは復元されるが、ヘルスケアのパーミッションは復元されない
- ユーザが機種変更したときなどヘルスケアのパーミッションだけがリセットされた状態になる

設定アプリ→[ヘルスケア]→[データアクセスとデバイス] で確認するとAPPが消えている（例）
<img width="320" alt="sample.image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72194/c6853bd3-c5b0-054a-2506-8c1e4fec8c5a.png">

# おわり

Appleもすごいヘルスケアに力を入れていて扱えるデータの種類もどんどん増えているので、これからアプリを作ってみたい人のお役に立てば幸いです！

また [DeNA 公式 Twitter アカウント @DeNAxTech](https://twitter.com/DeNAxTech) では色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい！