HealthKit: Daily Heart Rate Average
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
.
Query to Healthstore for Resting Heart Rate not returning any values
Turns out restingHeartRate
has its own permission that is required (in addition to heartRate
):
private func requestAccessToHealthKit() {
let healthStore = HKHealthStore()
let allTypes = Set([HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
healthStore.requestAuthorization(toShare: allTypes, read: allTypes) { (success, error) in
if !success {
}
}
}
Getting new heart rate data live from Health App?
You can use an HKAnchoredObjectQuery
to create a query that returns an initial set of data and then updates to the data set.
Unfortunately, you can't provide a sort descriptor to the HKAnchoredObjectQuery
, so you need to sort the data after you receive it if you don't want ascending order.
Here is a model object I created so that I could test in SwiftUI.
It creates an HKAnchoredQuery
and sets an update handler function. The update handler converts the HealthKit results into my HeartRateEntry
struct (This is so I could easily display the data in a SwiftUI list). The array is then sorted by descending date.
The update function stores the newAnchor
that was received so that only changes are delivered in the future.
While testing I found that running the heart rate app on my watch, moving my test app into the background and then swapping back to it triggered the new heart rate data more quickly than just waiting for the new data to be delivered.
import Foundation
import HealthKit
struct HeartRateEntry: Hashable, Identifiable {
var heartRate: Double
var date: Date
var id = UUID()
}
class HeartHistoryModel: ObservableObject {
@Published var heartData: [HeartRateEntry] = []
var healthStore: HKHealthStore
var queryAnchor: HKQueryAnchor?
var query: HKAnchoredObjectQuery?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
} else {
fatalError("Health data not available")
}
self.requestAuthorization { authorised in
if authorised {
self.setupQuery()
}
}
}
func requestAuthorization(completion: @escaping (Bool) -> Void){
let heartBeat = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
self.healthStore.requestAuthorization(toShare: [], read: [heartBeat]) { (success, error) in completion(success)
}
}
func setupQuery() {
guard let sampleType = HKObjectType.quantityType(forIdentifier: .heartRate) else {
return
}
let startDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: .distantFuture, options: .strictEndDate)
self.query = HKAnchoredObjectQuery(type: sampleType, predicate: predicate, anchor: queryAnchor, limit: HKObjectQueryNoLimit, resultsHandler: self.updateHandler)
self.query!.updateHandler = self.updateHandler
healthStore.execute(self.query!)
}
func updateHandler(query: HKAnchoredObjectQuery, newSamples: [HKSample]?, deleteSamples: [HKDeletedObject]?, newAnchor: HKQueryAnchor?, error: Error?) {
if let error = error {
print("Health query error \(error)")
} else {
let unit = HKUnit(from: "count/min")
if let newSamples = newSamples as? [HKQuantitySample], !newSamples.isEmpty {
print("Received \(newSamples.count) new samples")
DispatchQueue.main.async {
var currentData = self.heartData
currentData.append(contentsOf: newSamples.map { HeartRateEntry(heartRate: $0.quantity.doubleValue(for: unit), date: $0.startDate)
})
self.heartData = currentData.sorted(by: { $0.date > $1.date })
}
}
self.queryAnchor = newAnchor
}
}
}
How to get Heart Rate Data near by Real Time from Health Kit in iOS?
Here is my own analysis regarding get nearby real-time Heart Rate.
1. If you are accessing Health Kit data using iPhone app, In this scenario, Health Kit DB NOT frequently updated/refreshed. So, your app not able to get real-time latest updated data through iPhone app.
2. Using a watch app, you can access near real-time data through Health Kit DB. Watch app is able to get the real-time latest updated Health Kit Data.
3. You need to transfer data from watch to iPhone app. Here is a code for your reference. You can write code as per your requirement. You just need to access Heart Rate through HKQuery
let defaultSession = WCSession.default
let healthStore = HKHealthStore()
var currentHeartRateSample : [HKSample]?
var currentHeartLastSample : HKSample?
var currentHeartRateBPM = Double()
//Get Heart Rate from Health Kit
func getCurrentHeartRateData(){
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month, .day], from: Date())
let startDate : Date = calendar.date(from: components)!
let endDate : Date = calendar.date(byAdding: Calendar.Component.day, value: 1, to: startDate as Date)!
let sampleType : HKSampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
let predicate : NSPredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
let anchor: HKQueryAnchor = HKQueryAnchor(fromValue: 0)
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: predicate, anchor: anchor, limit: HKObjectQueryNoLimit) { (query, samples, deletedObjects, anchor, error ) in
if samples != nil {
self.collectCurrentHeartRateSample(currentSampleTyple: samples!, deleted: deletedObjects!)
}
}
anchoredQuery.updateHandler = { (query, samples, deletedObjects, anchor, error) -> Void in
self.collectCurrentHeartRateSample(currentSampleTyple: samples!, deleted: deletedObjects!)
}
self.healthStore.execute(anchoredQuery)
}
//Retrived necessary parameter from HK Sample
func collectCurrentHeartRateSample(currentSampleTyple : [HKSample]?, deleted : [HKDeletedObject]?){
self.currentHeartRateSample = currentSampleTyple
//Get Last Sample of Heart Rate
self.currentHeartLastSample = self.currentHeartRateSample?.last
if self.currentHeartLastSample != nil {
let lastHeartRateSample = self.currentHeartLastSample as! HKQuantitySample
self.currentHeartRateBPM = lastHeartRateSample.quantity.doubleValue(for: HKUnit(from: "count/min"))
let heartRateStartDate = lastHeartRateSample.startDate
let heartRateEndDate = lastHeartRateSample.endDate
//Send Heart Rate Data Using Send Messge
DispatchQueue.main.async {
let message = [
"HeartRateBPM" : "\(self.currentHeartRateBPM)",
"HeartRateStartDate" : "\(heartRateStartDate)",
"HeartRateEndDate" : "\(heartRateEndDate)"
]
//Transfer data from watch to iPhone
self.defaultSession.sendMessage(message, replyHandler:nil, errorHandler: { (error) in
print("Error in send message : \(error)")
})
}
}
}
Related Topics
Finding It Difficult to Pass Data to Separate Viewcontroller
In Collectionview How to Set Colors According to Selection
iOS Swift Connect Wifi Programmatically and Distinguish Between Bad Password and No Wifi in Range
How to Retrieve Image Stored in Firebase to Show It in View Image View
Openurl in Appdelegate Conversion Error Nsstring -> String (Swift & iOS8)
Does Not Have a Member 'Instantiatewithowner'
How to Get All Keys and Values into Separate String Arrays from Nsdictionary in Swift
How to Draw Text in Center to Uiimage
iOS 10 Issue: Uiscrollview Not Scrolling, Even When Contentsize Is Set
Buttonwithtype' Is Unavailable: Use Object Construction 'Uibutton(Type:)
Why am I Getting an Error Regarding Bolts Framework and Facebooksdk When I'm Not Even Using Bolts
How to Determine If a Business Is Open Given the Hours of Operation (Swift-Ios)
Returning Data from Function in Firebase Observer Code Block Swift
How to Draw Geojson in Apple Maps as Overlay Using Swift 3
Spritekit/Swift - How to Check Contact of Two Nodes When They Are Already in Contact