HKAnchoredObjectQuery not returning reliably in background - ios

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.

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.

Send request in applicationWillTerminate

In my app I need to send some instructions to server when the user terminated an app. In applicationWillTerminate func I tried to send it, but it never came to server. I tried to use Alamofire and native URLSession but it doesn't work. Does anybody know how can I send it?
I use this code
let request = "\(requestPrefix)setDriverOrderStatus"
if let url = URL(string:request) {
var parameters : [String : String] = [:]
parameters["access_token"] = UserSession.accessToken
parameters["driver_id"] = UserSession.userID
parameters["status"] = status
var req = URLRequest(url: url)
req.httpMethod = HTTPMethod.put.rawValue
do {
req.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
_ = URLSession.shared.dataTask(with: req, completionHandler: { data, response, error in
guard error == nil else {
print(error ?? "error")
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
print(json)
}).resume
}
One solution that worked for me is to add sleep at the end of the applicationWillTerminate function like this :
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
// HERE YOU will make you HTTP request asynchronously
self.postLogoutHistory()
// 3 is the number of seconds in which you estimate your request
// will be finished before system terminate the app process
sleep(3)
print("applicationWillTerminate")
// self.saveContext()
}
put breakpoint in applicationWillTerminate and check that, function is getting called or not because applicationWillTerminate is not called everytime when application is getting terminated, especially when user quit application manually from multitasking window, applicationWillTerminate will not get called! When system terminates the application at that time applicationWillTerminate will get called and you will got approximately five seconds to complete your task!! So, it is not good idea to perform network related task on applicationWillTerminate!!
Refer Apple Documentation for applicationWillTerminate, It states,
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
If the method does not return before time expires, the system may kill
the process altogether.
For apps that do not support background execution or are linked
against iOS 3.x or earlier, this method is always called when the user
quits the app. For apps that support background execution, this method
is generally not called when the user quits the app because the app
simply moves to the background in that case. However, this method may
be called in situations where the app is running in the background
(not suspended) and the system needs to terminate it for some reason.
After calling this method, the app also posts a
UIApplicationWillTerminate notification to give interested objects a
chance to respond to the transition.

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.

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.

Apple HealthKit - background updates not triggering

I am trying to perform some actions triggered by changes to Apple Health Kit, triggered in the background of my Swift iOS app.
Here's my AppDelegate:
var healthManager : HealthManager?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
healthManager = HealthManager.sharedInstance
return true
}
And in the initialization of the HealthManager class I authorize use of Health Kit and call:
var sampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
var predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.StrictStartDate)
var query = HKObserverQuery(sampleType: sampleType, predicate: predicate, updateHandler: stepsChangedHandler)
healthKitStore.executeQuery(query)
healthKitStore.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate, withCompletion: {(succeeded, error) in
if succeeded {
println("Enabled background delivery of step changes")
} else {
if let theError = error {
print("Failed to enable background delivery of step changed. ")
println("Error = \(theError)")
}
}
})
This works beautifully when the app is open- the stepsChangedHandler is called when there's a health kit update, but when the app is out of focus it never is called. I've searched around and found a few ideas, but none of the fixes have seemed to work for me.
Thanks!
What you have should work, but my experience with the simulator up through iOS 8.4 and Xcode 6.4 is that the background updates are not triggered. However, in my testing this does work on a device. To try for yourself, hook up and run your app on a device then switch to Health.app and add a relevant data point.
If your query is set for immediate updates, you should see your log message in the console. Make sure stepsChangedHandler includes completionHandler().
According to the docs, the query runs on a separate background thread so your appdelegate code will only be called on initial launch.
In the documentation for the HKHealthStore Class, under enableBackgroundDeliveryForType:... there is a paragraph:
Some data types, such as step counts, have a minimum frequency of HKUpdateFrequencyHourly. This frequency is enforced transparently.
which explains why you won't see background updates as frequently as you are specifying. I'm not sure if theres a listing of which data types are included in the "some" quantifier.

Resources