HKStatisticsCollectionQuery について

HKStatisticsCollectionQueryとは
HealthKit
のAPIの一つであり、一定の間隔で複数の統計用のQueryを実行するQueryです。
HKStatisticsCollectionQuery
はグラフやチャートのデータを生成するためによく使われます。
e.g. 1日ごとの合計歩数の取得、1時間毎の平均心拍数の取得 etc.
また新規のヘルスデータの作成や削除など、ヘルスデータの変更を監視できます。

HKStatisticsCollectionQueryのイニシャライザ
HKStatisticsCollectionQueryのイニシャライザのシグネチャは以下のようになっています。
init(
quantityType: HKQuantityType,
quantitySamplePredicate: NSPredicate?,
options: HKStatisticsOptions = [],
anchorDate: Date,
intervalComponents: DateComponents
)
個人的に分かりにくいと感じたのがanchorDate
の引数です。
anchorDate
の概念を考えるにあたって、時間を以下のような数直線と考えてください。
HKStatisticsCollectionQuery
はanchorDate
とintervalComponents
を用いて一致するサンプルデータ分割します。
anchorDate
で任意の開始点を定義しますが、開始点はさほど重要ではないことが多いです。
なぜならTimeIntervalはanchor pointから両方向に広がるため、どこをanchor pointにしても結局はすべての時間が含まれるためです。
重要なのは
例として1日ごとの歩数を取得してみます。
1日ごとなのでTimerIntervalは1日ですが、アンカーポイントは1970年1月1日午前3時34分または2065年3月15日午前3時34分である可能性があります。どちらのアンカーポイントでもクエリは一致するサンプルデータを日単位で分割し、毎日午前3時34分を分割の始点とします。

直近3ヶ月の1週間ごとの歩数の合計を取得してみる
まずはアンカーポイントと時間間隔を作成します。
let calendar = Calendar.current
// 1週間 == 7日間なので、間隔を7日で指定します。
let interval = DateComponents(day: 7)
// アンカーポイントの日付を月曜日の午前3時にしていします。
let components = DateComponents(
calendar: calendar,
timeZone: calendar.timeZone,
hour: 3,
minute: 0,
second: 0,
weekday: 2
)
// アンカーポイントを直前の月曜午前3時にします。
guard let anchorDate = calendar.nextDate(
after: Date(),
matching: components,
matchingPolicy: .nextTime,
repeatedTimePolicy: .first,
direction: .backward
) else { fatalError() }
次に先程作成した要素を用いて、HKStatisticsCollectionQuery
を作成します。
let query = HKStatisticsCollectionQuery(
quantityType: .quantityType(forIdentifier: .stepCount)!,
quantitySamplePredicate: nil,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: interval
)
クエリの実行結果を受け取るコールバックハンドラーを指定します。
query.initialResultsHandler = { query, collection, error in
guard let statsCollection = collection else { return }
let endDate = Date()
let threeMonthAgo = DateComponents(month: -3)
// 現時点から3ヶ月前のDateオブジェクトを取得。
guard let startDate = Calendar.current.date(byAdding: threeMonthAgo, to: endDate) else { fatalError() }
// enumerateStatisticsで指定した範囲の統計値を取得します。
// 今回の場合、3ヶ月前から現時点までの統計値を取得します。
// つまり直近から3ヶ月前の1週間毎の歩数の合計
statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in
if let quantity = stats.sumQuantity() {
let date = stats.startDate
let value = quantity.doubleValue(for: .count())
print("date: \(date), stepCount: \(value)")
}
}
}