iOSで正しく歩数を取得する

この記事は、以下の資料をqiita用の記事にしたものです。
https://speakerdeck.com/satoshin/getting-step-count-on-ios

## はじめに

iOSで歩数を取得する方法には以下の二つの方法があります。

- CMPedometerから取得する方法
- HealthKitから取得する方法

HealthKitの方では、取得方法を間違えると実際のデータと大きく異なるので注意してください。

## CMPedometerから取得する

```swift
let pedometer = CMPedometer()
 pedometer.queryPedometerData(from: start, to: end) { (data, error) in
    print(data?.numberOfSteps)
}
```

* start ~ end までの間で端末で検知した歩数を取得
    * AppleWatchの情報取得はWatchAppで実装する必要あり
* 取得できるのは過去7日分

## HealthKitから取得する

### :warning: `HKSampleQuery` で取得する

HealthKitでは、いつ、何時に何歩歩いたかと言う情報をHealthStoreに保持しています。
HealthStoreからデータを取得する方法の一つに`HKSampleQuery`を利用できます。
取得したデータの合計値を計算すれば良いかと考えられますが、この方法はやってはいけない**ダメな**方法です。

なぜなら、HealthStoreには**iPhone**と**Apple Watch**が自動記録した歩数データが入っているため、
素直に足し合わせてしまうと、二つの歩数計データを足し合わせてしまうことになります。

iPhoneだけでテストしてしまうと、一見正しい情報に見えてしまうため注意です。

```swift:間違った取得方法-HKSampleQueryで取得した合計値を表示する
let store = HKHealthStore()
let type = HKSampleType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForSamples(withStart: start, end: end)
let query = HKSampleQuery(sampleType: type, 
                          predicate: predicate, 
                          limit: HKObjectQueryNoLimit, 
                          sortDescriptors: nil) { (query, result, error) in
    let result = result as! [HKQuantitySample]
    let sum = result(0) { $0 + $1.quantity.doubleValue(for: .count()) }
    print(sum)
}
store.execute(query)
```

### :o: `HKStatisticsQuery` / `HKStatisticsCollectionQuery` で取得する

多くの場合において、一つ一つの歩数を取得したいのではなく、データの特徴を知りたいはずです。
この場合は `HKStatisticsQuery` / `HKStatisticsCollectionQuery` を利用しましょう。

以下のサンプルでは、`HKStatisticsQuery`を利用しています。特定のイベントで取得したい場合にはこれで十分です。
が、 `HKStatisticsCollectionQuery` では データ更新のハンドリングができ、取得時間範囲の指定にDateComponentが使えるので、こちらの方が便利な場合も多いです。

```swift:HKStatisticsQueryで歩数を取得する
let store = HKHealthStore()
let type = HKSampleType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForSamples(withStart: start, end: end) 
let query = HKStatisticsQuery(quantityType: type, 
                              quantitySamplePredicate: predicate, 
                              options: .cumulativeSum) { (query, statistics, error) in
     print(statistics?.sumQuantity())
}
store.execute(query)
```

### 不正対策

HealthKitから取得する歩数は、時間がかぶっていなければサードパーティのアプリやHealthAppから手動で登録した値も加算されます。
これでは、歩いた歩数がポイントになるアプリなどでは、歩いていないのに多くのポイントを入手することが可能になってしまいます。

HealthStoreに保存された各データには、登録したアプリのBundle Identifierのデータも一緒に書き込まれています。
よってこれでフィルタリングすれば、iPhone、AppleWatchの自動歩数記録機能から保存されたデータのみを抽出することができます。

自動歩数記録機能で記録されたデータとHealthAppのBundle Identifierは以下です。

- 自動歩数記録機能: `com.apple.health.[Device ID]` (Device ID部分は端末によって変わります)
- HealthApp: `com.apple.Health`

よって、`hasPrefix("com.apple.health")`で判定してあげればOKです。


```swift:自動記録された歩数だけを取得する
let store = HKHealthStore()
let type = HKSampleType.quantityType(forIdentifier: .stepCount)!
let datePredicate = HKQuery.predicateForSamples(withStart: start, end: end)
let query = HKStatisticsQuery(quantityType: type, 
                              quantitySamplePredicate: datePredicate, 
                              options: .separateBySource)
    { (query, data, error) in
    if let sources = data?.sources?.filter({ $0.bundleIdentifier.hasPrefix("com.apple.health") }) {
        let sourcesPredicate = HKQuery.predicateForObjects(from: Set(sources))
        let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [datePredicate, sourcesPredicate])
        let query = HKStatisticsQuery(quantityType: type, 
                                      quantitySamplePredicate: predicate, 
                                      options: .cumulativeSum) 
        { (query, statistics, error) in
             print(statistics?.sumQuantity())
        }
       store.execute(query)
    }
}
store.execute(query)
```

### サンプルコード
https://github.com/sato-shin/ios-steps