『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

HealthKitを使って心拍数を取得してグラフとか出したやつ

はじめに

先週のブログで予告してしまったので HealthKit を使って心拍数を取得してグラフを表示させてみました。

f:id:nanashinodonbee:20180513002236p:plain

最終的にはこんな画面を作成しました。

先日行われた #kosen10s のLT12にてデモと発表(?)という感じでお話をしてきました。

kosen10s.connpass.com

環境

  • 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 の中には nilaverageQuantity() があるのでそれは弾いたりした方がいいかもしれません。

Apple Developer Documentation

グラフ表示

github.com

今回は、グラフ表示は上のライブラリを使用させていただきました。

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からワークアウトを取得してデータを表示してみるというような一連の動作をしてみました。

基本的に大きな変更がなかったので古いネットの情報でもこれぐらいのことはできたのでよかったです。

ちょっと記事を書くのが面倒になってきたので今日はこのぐらいで終わります!!