Search code examples
iosswifthealthkitwatchos

Getting new heart rate data live from Health App?


I'm trying to build an iPhone app based on the user's live heart rate data. I was able to get the latest heart rate from the Health App but once new information is recorded the app is not updated until relaunch.

Is there a way to get heart rate data in live? Maybe with HKObserverQuery?

here's my code so far: (Who manages to pull the last heartbeat)

import Foundation
import UIKit
import HealthKit


class HealthStore {

var healthStore: HKHealthStore?

init() {
    if HKHealthStore.isHealthDataAvailable(){
        healthStore = HKHealthStore()
    }
}

func requestAuthorization(completion: @escaping (Bool) -> Void){
    let heartBeat = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!

    guard let healthStore = self.healthStore else {return completion(false)}

    healthStore.requestAuthorization(toShare: [], read: [heartBeat]) { (success, error) in completion(success)
    }
}

func latestHarteRate(){
    

    
    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: Date(), options: .strictEndDate)
    
    let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
    
    let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: Int(HKObjectQueryNoLimit), sortDescriptors: [sortDescriptor]){(sample,result,error) in guard error == nil else{
            return
        }

    let data = result![0] as! HKQuantitySample
    let unit = HKUnit(from: "count/min")
    let latestHr = data.quantity.doubleValue(for: unit)
    print("Latest Hr\(latestHr) BPM")

    healthStore?.execute(query)
}

Solution

  • 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
            }
            
            
        }
    }