I am trying to build a watchOS 2 complication that displays a user's health data when the watch is unlocked, 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 query 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 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 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, so 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.
So, 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. Instead of calling the query directly 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)
}
}
This code works as long as the watch extension is in the forground. 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?