Apple Watch HeathKit observe Workout-App workout finished - ios

For my App on Apple Watch I want to observe when a workout in Workout-App was completed, to do a background update of my App and re-render the Complication. But the Query never fires.
My Approach was to use an HKObserverQuery and observe .workoutType(). Documentation mentions:
HealthKit uses workout types to create samples that store information about individual workouts.
On https://developer.apple.com/documentation/healthkit/hkobjecttype/1615132-workouttype. So that felt like the right sampleType.
I added com.apple.developer.healthkit.background-delivery to the Entitlements of the WatchApp Extension and Workout access permissions are setup properly.
That is the code, I am using:
let query = HKObserverQuery(sampleType: .workoutType(), predicate: nil, updateHandler: { [weak self] _, completionHandler, error in
guard error == nil else {
// CoreInsights.logs.track("HK Query Error", level: .error)
return
}
// CoreInsights.logs.track("Update Trigger HK", level: .info)
// self?.performBackgroundTasks(completion: {
// completionHandler()
// })
})
self?.healthStore.execute(query)
self?.healthStore.enableBackgroundDelivery(for: .workoutType(), frequency: .immediate, withCompletion: { updateSuccess, _ in
// CoreInsights.logs.track("HK Background \(updateSuccess)", level: .info)
})
healthStore.enableBackgroundDelivery returns with success true.
Someone knows, if I am using the right type and setup background correctly?

Related

HKObserverQuery background delivery stops working after calling completionHandler

So according to Apple, I need to call the HKObserverQueryCompletionHandler after the updateHandler is triggered from data being added to HK.
But as soon as I call the completionHandler, the observer query stops giving any more updates in the background..
Here is my code:
guard let sampleType = sample as? HKQuantityType else { return nil }
let query = HKObserverQuery(sampleType: sampleType, predicate: nil, updateHandler: { query, completionHandler, error in
completionHandler()
IamExecutingHKStatisticsCollectionQueryHere()
})
healthStore?.execute(query)
healthStore?.enableBackgroundDelivery(for: sampleType, frequency: .hourly, withCompletion: { success, error in
})
If I don't call the completionHandler, everything works fine but I have never tested for long periods of time..
Calling completionHandler() indicates that you are done processing new data. Only call it once you have processed the results of the queries you execute in response to updateHandler being called. If you call completionHandler() early as you are now, the system will stop running your app in the background before you have a chance to process data.

HKObserverQuery in Background mode

I have an application that need to track user heart rate readings from apple watch, so I did all the required steps that I found on apple guides, and here is the code that I am using:
static var query: HKObserverQuery?
func startObservingHeartRate() {
guard let heartRateSampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate) else {
fatalError("Unable to create a step count sample type")
}
AppDelegate.query = HKObserverQuery(sampleType: heartRateSampleType, predicate: nil, updateHandler: { (query, completionHandler, error) in
if error != nil {
// Perform Proper Error Handling Here...
print("An error occured while setting up the Heart Rate observer.")
}
//Read the last strored heatt rate in add it to the DB
//Add last fetched Heart Rate reading to DB and send it to clips
HealthKitManager().fetchLastStoredHeartRate(completion: { (lastReading, error) in
guard let lastReading = lastReading else {
//There is no heart readings in HealthKit
return
}
//Check if Last HR value is Abnormal
if lastReading.doubleValue > 60 {
//TODO: - Schedule notification
if UIApplication.shared.applicationState == .background {
} else {
//TODO: - Show popup to the user
}
}
})
completionHandler()
})
healthKitStore.execute(AppDelegate.query!)
configureHeartRateObserver()
}
func configureHeartRateObserver() {
guard let heartRateSampleType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate) else {
fatalError("Unable to create a step count sample type")
}
healthKitStore.enableBackgroundDelivery(for: heartRateSampleType, frequency: HKUpdateFrequency.immediate) { (success, error) in
if success {
print("Enabled background delivery of Heart Rate changes")
} else {
print("Failed to enable background delivery of weight changes. ")
}
}
}
and I am calling "startObservingHeartRate" in didFinishLaunchingWithOptions in AppDelegate, assuming that this query should be executed once a new reading added or deleted from the health kit store, every thing is fine, if app is in background or killed the handler wake up my app and it do the updates.
But whenever I put the app in background then put it in foreground again it execute the observer query for many times even if there is no new readings added to the HealthKit store and in this case I am getting the same last heart rate for many times for no reason.
Please any recommendation on how to use this types of query or any changes I need to do with my current implementation.
If you want to track added and removed heart rate samples more precisely, you should use an HKAnchoredObjectQuery. HKObserverQuery does not guarantee that its update handler will only be called when a sample is added or removed. Note that you must continue executing an HKObserverQuery in addition to HKAnchoredObjectQuery since you are also using enableBackgroundDelivery(for:frequency:completion:).

What's the logic in HKObserverQuery background delivery?

I need my app to sync between HealthKit and our database while it's in the background. I just can't wrap my head around the logic that determines how and when the HKObserverQueries run their updateHandlers. I need data of various different sample types, so I assume I need an observer query for each one. Right?
According to Apple about function enableBackgroundDeliveryForType, "HealthKit wakes your app whenever new samples of the specified type are saved to the store." But if I enable background deliveries and execute observerqueries for, say, blood glucose and weight, they both seem to run their updatehandlers whenever I input data in either one of them in the Health app. This also seems to happen even when I enable background delivery for only one of the sample types. Why?
func startObserving(completion: ((success: Bool) -> Void)!) {
let sampleTypeBloodGlucose = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
let sampleTypeWeight = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass)!
// Enable background delivery for blood glucose
self.healthKitStore.enableBackgroundDeliveryForType(sampleTypeBloodGlucose, frequency: .Immediate) {
(success, error) in
if error != nil {
abort()
}
}
// Enable background delivery for weight
self.healthKitStore.enableBackgroundDeliveryForType(sampleTypeWeight, frequency: .Immediate) {
(success, error) in
if error != nil {
abort()
}
}
// Define update handlers for background deliveries
let updateHandlerBloodGlucose: (HKObserverQuery, HKObserverQueryCompletionHandler, NSError?) -> Void = {
query, completionHandler, error in
if error != nil {
abort()
}
// Handle data and call the completionHandler
completionHandler()
}
let updateHandlerWeight: (HKObserverQuery, HKObserverQueryCompletionHandler, NSError?) -> Void = {
query, completionHandler, error in
if error != nil {
abort()
}
// Handle data and call the completionHandler
completionHandler()
}
let observerQueryBloodGlucose = HKObserverQuery(sampleType: sampleTypeBloodGlucose, predicate: nil, updateHandler: updateHandlerBloodGlucose)
healthKitStore.executeQuery(observerQueryBloodGlucose)
let observerQueryWeight = HKObserverQuery(sampleType: sampleTypeWeight, predicate: nil, updateHandler: updateHandlerWeight)
healthKitStore.executeQuery(observerQueryWeight)
completion(success: true)
}
If you are using the background delivery feature of HealthKit, then yes, you do need to open an HKObserverQuery for each type of data you observe and handle the invocations of the updateHandler and call the provided completion when you are finished. However, the updateHandler of HKObserverQuery is advisory and invocations do not necessarily correspond one-to-one with changes to the HealthKit database (there is not always enough information available to determine what your app has processed and what it hasn't, so sometimes the handler may run when there isn't new data).
Don't worry about understanding or controlling precisely when updateHandler runs - just use it as a trigger to perform other queries that will actually give you up-to-date values from HealthKit. If you need to know precisely which samples in HealthKit are new, for example, then your app should use an HKAnchoredObjectQuery.

HKAnchoredObjectQuery not returning reliably in background

I'm trying to make my app synchronize HealthKit data with our database whenever new data input is observed. Despite some ambiguity in the logic I believe I've managed to enable background delivery for the sample types and have the observers react when needed.
However, in the observer's updatehandler I need to create an HKAnchoredObjectQuery for fetching the most recent results from HealthKit, but these queries don't return reliably when my app is running in the background. Often when I add sample points in HealthKit, the anchored queries just get executed, but they return only when I bring the app back in the foreground.
Other times they return immediately without me having to activate the app. One possible issue is that all of my observer queries fire their updatehandlers, thus creating multiple AnchoredObjectQueries that may or may not return. I've explained that further in the linked thread, because it may be unrelated.
Here's an example of the function I'm calling from HKObserverQuery's updatehandler:
func synchronizeRecentData(sampleType: HKSampleType, observerQuery: HKObserverQuery) {
let completionHandler: (HKAnchoredObjectQuery, [HKSample]?, Int, NSError?) -> Void = {
[unowned self] query, results, newAnchor, error in
if error != nil {
abort()
}
// Update queryAnchor
self.queryAnchors[sampleType] = newAnchor
guard let receivedNewSamples = results as? [HKQuantitySample] else {
abort()
}
// Handle received samples here
}
let query = HKAnchoredObjectQuery(type: sampleType,
predicate: nil,
anchor: self.queryAnchors[sampleType]!,
limit: HKObjectQueryNoLimit,
completionHandler: completionHandler)
healthKitStore.executeQuery(query)
}
According to the HealthKit docs:
Unlike the observer query, these updates include a list of items that
have been added or removed; however, anchored object queries cannot be
registered for background delivery. For more information, see
HKAnchoredObjectQuery.
The only query that can register for background delivery is the HKObserverQuery
I believe this is because when your app gets woken up in the background you have very limited time to execute before the app gets suspended again. If you use UIBackgroundTaskIdentifier you should be able to ensure that your app runs long enough to finish getting the results of the HKAnchoredObjectQuery.

How can you display HealthKit data on a complication that is refreshed in the background?

I am trying to build a watchOS 2 complication that displays a user's health data, such as steps (but in theory it should be able to display any health data the user has given the app permission to view). When the complication first launches, I can query Healthkit and get all the data I want because the first launch is considered to be in the foreground. However, I am having trouble retrieving the HealthKit data in the background when new health data is available. There are two places I could get this data, the watch and the iPhone.
I have tried to get the data from the watch itself when the complication's background refresh is triggered from the date set in getNextRequestedUpdateDateWithHandler. However, when I call HKHealthStore's execute method, it does not return any query results if the app (or in this case the complication) is running the background. I have also tried to setup an HKAnchoredObject query that should return my results immediately when the process resumes, but this also doesn't seem to return any results unless I manually launch the app extension on the watch. Here is my watch code, called from my ExtensionDelegate's init method after health kit permissions are requested:
func setupComplicationDataCache() {
let now = NSDate()
var startDate: NSDate? = nil
var interval: NSTimeInterval = 0
self.calendar.rangeOfUnit(NSCalendarUnit.Day, startDate: &startDate, interval: &interval, forDate: now)
let stepSampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!
// Match samples with a start date after the workout start
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: nil, options: .None)
let query = HKAnchoredObjectQuery(type: stepSampleType, predicate: predicate, anchor: nil, limit: 0) { (query, samples, deletedObjects, anchor, error) -> Void in
// Handle when the query first returns results
self.handleStepResults(query, samples: samples, deletedObjects: deletedObjects, anchor: anchor, error: error)
}
query.updateHandler = { (query, samples, deletedObjects, anchor, error) -> Void in
// Handle update notifications after the query has initially run
self.handleStepResults(query, samples: samples, deletedObjects: deletedObjects, anchor: anchor, error: error)
}
self.healthStore.executeQuery(query);
}
func handleStepResults(query: HKAnchoredObjectQuery, samples: [HKSample]?, deletedObjects: [HKDeletedObject]?, anchor: HKQueryAnchor?, error: NSError?) {
if error != nil {
self.timelineModel.currentEntry = TimelineEntryModel(value: NSNumber(int: -1), endDate: NSDate())
} else if samples == nil || samples?.count == 0 {
self.timelineModel.currentEntry = TimelineEntryModel(value: NSNumber(int: 0), endDate: NSDate())
} else {
let newStepSamples = samples as! [HKQuantitySample]
var stepCount = self.timelineModel.currentEntry.value.doubleValue
var currentDate = self.timelineModel.currentEntry.endDate
// Add the current entry to the collection of past entries
self.timelineModel.pastEntries.append(self.timelineModel.currentEntry)
// Add all new entries to the collection of past entries
for result in newStepSamples {
stepCount += result.quantity.doubleValueForUnit(self.countUnit)
currentDate = result.endDate
self.timelineModel.pastEntries.append(TimelineEntryModel(value: NSNumber(double: stepCount), endDate: currentDate))
}
// Retrieve the latest sample as the current item
self.timelineModel.currentEntry = self.timelineModel.pastEntries.popLast()
if self.timelineModel.currentEntry == nil {
self.timelineModel.currentEntry = TimelineEntryModel(value: NSNumber(int: -3), endDate: NSDate())
}
}
// Reload the complication
let complicationServer = CLKComplicationServer.sharedInstance()
for complication in complicationServer.activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
I have also tried to get the data from the iPhone using HKObserverQuery. I have the observer query that can wake up the iPhone once an hour (the max time for step data). However, if the iPhone is locked when the observer completion handler executes my step query, HKHealthStore's execute method also refuses to return any query results. I think this makes sense here and there is probably not a way around this because Apple's docs mention that the Health Store is encrypted when a device is locked and you can't read from the store (only write). BUT in the watch's case when it is on someones wrist it is not locked, the screen is just turned off.
Does anyone know how to get HealthKit updates to show up on a complication when a refresh happens in the background, either in iOS or on watchOS 2?
After extensive testing I have determined that this currently is not possible. On watchOS 2, Apple seems to have completely disabled HealthKit queries from returning results when an extension or complication is running in the background. This includes execution from remote notifications, Watch Connectivity, and from the complications scheduled refresh. The iPhone's HealthKit queries fail if the screen is off and the device has a passcode set. The queries fail because the health data store is encrypted when the device is locked. The queries fail even if observer queries and background delivery is enabled. You can get notified that something changed, but you can't query for the changes until the iPhone is unlocked (because again, the data is encrypted).
Other apps that show healthkit related data, such as steps and walk+run distance do so by querying the pedometer (CMPedometer) directly, whose data is accessible in these background modes.
One could make a complication that updates in the background exclusivly for iPhone users who do not have a passcode set on their device, but this seems like a terrible idea to promote.

Resources