Search code examples
iosswiftxcodehealthkit

HealthKit: Daily Heart Rate Average


I am trying to get the average heart rate per day from a user's device. I have the code below which prints "avgHeart" when called, but doesn't do anything else.

I am testing it on my own phone which definitely has heart rate data and has been authorised to read heart rate data.

func fortnightAvgHeartRate() {
    print("avgHEART")
    let calendar = Calendar.current

    var interval = DateComponents()
    // 14-day time interval
    interval.day = 1

    // Set the anchor date to Monday at 3:00 a.m.
    let anchorDateTMP = calendar.date(byAdding: .day, value: -14, to: Date())!
    let anchorDate = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: anchorDateTMP)!

    guard let quantityType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
        fatalError("*** Unable to create a step count type ***")
    }

    // Create the query
    let query = HKStatisticsCollectionQuery(quantityType: quantityType,
                                            quantitySamplePredicate: nil,
                                            options: .discreteAverage,
                                            anchorDate: anchorDate,
                                            intervalComponents: interval)

    // Set the results handler
    query.initialResultsHandler = {
        query, results, error in

        guard let statsCollection = results else {
            // Perform proper error handling here
            fatalError("*** An error occurred while calculating the statistics: \(error?.localizedDescription) ***")
        }

        let endDate = Date()

        guard let startDate = calendar.date(byAdding: .day, value: -14, to: endDate) else {
            fatalError("*** Unable to calculate the start date ***")
        }

        // Plot the weekly step counts over the past 3 months
        statsCollection.enumerateStatistics(from: startDate, to: endDate) { [unowned self] statistics, stop in

            if let quantity = statistics.sumQuantity() {
                let date = statistics.startDate
                let value = quantity.doubleValue(for: HKUnit.count())

                let formatter = DateFormatter()
                formatter.dateFormat = "ddMMMyyyy"
                let dateResult = formatter.string(from: date)

                print("HEART")
                self.addToArray(date: dateResult, value: value, number: 5)
            }
        }
    }

    HKHealthStore().execute(query)
}

The called function is as follows:

var dateArray: [String] = []
var avgHeartRateDataArray: [Double] = []

func addToArray(date: String, value: Double, number: Double) {
    // ...
    if number == 5 {
        avgHeartRateDataArray.append(value)
        print("avgHeartRate: \(avgHeartRateDataArray)")
    }

    if dateArray.contains(date) {
        // Do nothing
    } else {
        dateArray.append(date)
    }

    print("ARRAY1: \(dateArray)")
}

And in this, print("avgHeartRate: \(avgHeartRateDataArray)") is not called at all.

Ideally I'd like the avgHeartRateData array to be populated with the average heart rate for the past 14 days.


Solution

  • The main problem is that you're asking for statistics.sumQuantity(), which will always be nil for this query. Instead, you should ask for statistics.averageQuantity().

    Also, quantity.doubleValue(for: HKUnit.count()) will throw an error, because heart rate is not stored in counts, but in counts per unit of time. To get beats per minute, use HKUnit.count().unitDivided(by: HKUnit.minute()).

    This will get your code working, but you also really should limit your query for the dates you need. Don't run an unbounded query and then limit the results to fit your date frame, set the predicate to get only the statistics you need.

    Here is an example that incorporates everything I said above:

    func printFortnightAvgHeartRate() {
        let calendar = Calendar.current
    
        let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
    
        // Start 14 days back, end with today
        let endDate = Date()
        let startDate = calendar.date(byAdding: .day, value: -14, to: endDate)!
    
        let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
    
        // Set the anchor to exactly midnight
        let anchorDate = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: Date())!
    
        // Generate daily statistics
        var interval = DateComponents()
        interval.day = 1
    
        // Create the query
        let query = HKStatisticsCollectionQuery(quantityType: heartRateType,
                                                quantitySamplePredicate: predicate,
                                                options: .discreteAverage,
                                                anchorDate: anchorDate,
                                                intervalComponents: interval)
    
        // Set the results handler
        query.initialResultsHandler = { query, results, error in
            guard let statsCollection = results else { return }
    
            for statistics in statsCollection.statistics() {
                guard let quantity = statistics.averageQuantity() else { continue }
    
                let beatsPerMinuteUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
                let value = quantity.doubleValue(for: beatsPerMinuteUnit)
    
                let df = DateFormatter()
                df.dateStyle = .medium
                df.timeStyle = .none
                print("On \(df.string(from: statistics.startDate)) the average heart rate was \(value) beats per minute")
            }
        }
    
        HKHealthStore().execute(query)
    }
    

    And as a side note, in your addToArray function you seem to be using the number argument to differentiate between types of data. At least use Int for that, not Double, but ideally this should be an enum.