はじめに
先週のブログで予告してしまったので HealthKit
を使って心拍数を取得してグラフを表示させてみました。
最終的にはこんな画面を作成しました。
先日行われた #kosen10s のLT12にてデモと発表(?)という感じでお話をしてきました。
環境
- Xcode9.3
- Swift4.1
- iOS11.3
ちなみに
スライドもあるんでもしよかったら、Starほしい
許可画面を出すやつ
一応、ユーザの大切な情報を取得するため(場合によっては個人情報)に許諾画面を表示させてから取得する必要があります。
import HealthKit class ViewController: UIViewController { private let healthStore = HealthStore() // ワークアウトと心拍数を読み出しに設定 private let readDataTypes: Set<HKObjectType> = [ HKWorkoutType.workoutType(), HKObjectType.quantityType(forIdentifier: .heartRate)!, ] override func viewDidLoad() { { super.viewDidLoad() healthStore.requestAuthorization(toShare: nil, read: readDataTypes) { (success, error) in guard success, error == nil else { return } // do something... } } }
あんまり viewDidLoad()
でこの処理をするのは推奨できませんが、デモなので...
ワークアウトを取得する
private var workouts = [HKWorkouts]() private func getWorkouts() { let type = HKWorkoutType.workoutType() let predicate = HKQuery.predicateForWorkouts(with: .other) // その他のワークアウトを取得。他にはランニング等もあります let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) // 並び順は開始時間から let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor] { [unowned self] (query, samples, error) in guard let workouts = samples as? [HKWorkout], error == nil else { return } self.workouts = workouts // do something... } healthStore.execute(query) }
今回は、ランニング等のワークアウト情報は必要としないので その他 (.other)
のワークアウトを選択しました。
HKObjectQueryNoLimit
はHealthKitが持ってる変数で 0
(制限なし)が設定されています。
詳しいやつ↓
Apple Developer Documentation
心拍数を取得する
private var statistics = [HKStatistics]() private func getHeartRates(workout: HKWorkout) { guard let type = HKObjectType.quantityType(forIdentifier: .heartRate) else { return } let predicate = HKQuery.predicateForSamples(withStart: workout.startDate, end: workout.endDate, options: .strictStartDate) let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: [.discreteAverage, .discreteMin, .discreteMax]) { [unowned self] (query, statistic, error) in guard let statistic = statistic, error == nil else { return } self.statistics.append(statistic) print("最低値 \(statistic.minimumQuantity()?.doubleValue(for: HKUnit(from: "count/min")) ?? 0) bpm") print("最高値 \(statistic.maximumQuantity()?.doubleValue(for: HKUnit(from: "count/min")) ?? 0) bpm") print("平均値 \(statistic.averageQuantity()?.doubleValue(for: HKUnit(from: "count/min")) ?? 0) bpm") } healthStore.execute(query: query) }
ワークアウト( HKWorkout
)を指定して、 HKStatistics
を配列で取得する感じで、 平均値
, 最低値
, 最高値
を取得できるようにします。設定しなかった場合は、 nil
になります。
また、心拍数に 合計値
はないので .cumulativeSum
を指定した場合クラッシュしました。(2018年5月2日現在)
こういったメソッドを実装しました。
呼ぶ場所は別にどこでも良いとは思うんですけど、ワークアウトを取得したときにしました。
さっきワークアウトを取得するところの do something...
のところに以下のように追加しました。
self.workouts.forEach { self.getHeartRates(workout: $0) }
5分毎の心拍数を取得する
private var heartRateStatistics = [HKStatistics]() private func getHeartRateWithFiveMinutes(_ workout: HKWorkout) { var dateComponents = DateComponents() dateComponents.minute = 5 // 間隔時間 let quantityType = HKObjectType.quantityType(forIdentifier: .heartRate)!, let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: nil, options: [.discreteAverage, .discreteMin, .discreteMax], anchorDate: workout.startDate, intervalComponents: dateComponents) collectionQuery.initialResultsHandler = { [unowned self] (query, result, error) in guard let result = result, error == nil else { return } result.enumerateStatistics(from: workout.startDate, to: self.workout.endDate) { (statistic, stop) in self.heartRateStatistics.append(statistic) // do something... } } healthStore.execute(query: query) }
今回は 5分毎
で取得しましたが、時間がかかりすぎるかもしれませんが1分毎でもできるかと思います。
result.enumerateStatistics
の中には nil
の averageQuantity()
があるのでそれは弾いたりした方がいいかもしれません。
グラフ表示
今回は、グラフ表示は上のライブラリを使用させていただきました。
import Charts @IBOutlet weak var chartView: LineChartView! private func setDataSet() { // nilのものを予め削除しておく let statistics = heartRateStatistics.filter { $0.minimumQuantity() != nil || $0.maximumQuantity() || $0.averageQuantity() != nil } // 平均だけ取得しておきます let values: [ChartDataEntry] = (0..<statistics.count).map { let quantity: HKQuantity? = statistics[$0].averageQuantity() let value = quantity?.doubleValue(for: HKUnit(from: "count/min")) ?? 0 return ChartDataEntry(x: Double($0), y: Double(String(format: "%.2f", value))!) } let set = LineChartDataSet(values: values, label: "心拍数(5分毎の平均)") set.drawIconsEnabled = false set.lineDashLengths = [5, 2.5] set.highlightLineDashLengths = [5, 2.5] set.setColor(.black) set.setCircleColor(.black) set.lineWidth = 1 set.circleRadius = 3 set.drawCircleHoleEnabled = false set.valueFont = .systemFont(ofSize: 9) set.formLineDashLengths = [5, 2.5] set.formLineWidth = 1 set.formSize = 15 let gradientColors: [CGColor] = [UIColor.white.cgColor, UIColor.red.cgColor] let gradient = CGGradient(colorsSpace: nil, colors: gradientColors as CFArray, locations: nil)! set.fillAlpha = 1 // グラデーションの角度 set.fill = Fill(linearGradient: gradient, angle: 90) set.drawFilledEnabled = true let data = LineChartData(dataSet: set) DispatchQueue.main.async { self.chartView.isHidden = false self.chartView.data = data } }
ちょっと長いですが、こんな感じ。多分GitHub上のデモを使って簡単に実装できるかと思います。
最後に
HealthKitからワークアウトを取得してデータを表示してみるというような一連の動作をしてみました。
基本的に大きな変更がなかったので古いネットの情報でもこれぐらいのことはできたのでよかったです。
ちょっと記事を書くのが面倒になってきたので今日はこのぐらいで終わります!!