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)")
})
}
}
}
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
.
Related Topics
Finding It Difficult to Pass Data to Separate Viewcontroller
How to Add a Ibaction to a Button Programmatically in Swift 4
How to Get the Nondecoded Attributes from a Decoder Container in Swift 4
Swift Lazy and Optional Properties
Swift: Memory Not Clearing When I Segue to Another View Controller, Recieving Memory Warning
How to Get the Correct Current Time in iOS
How to Set Uitextview's Height Dynamically in Uitableviewcell Based on String Size
Uisearchbar Out of Screen Bounds When Navigation Bar Translucent = False
How to Create Gmsmarker with Combined Images in Swift
Buttonwithtype' Is Unavailable: Use Object Construction 'Uibutton(Type:)
Customise Uitabbar Height in Xcode11/Ios13 or 13.1
Cocoa Singleton and Shared Instances
Save Depth Images from Truedepth Camera
Adding Uigesturerecognizer to Subview Programmatically in Swift