Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 452
lehn0058 Avatar asked Mar 14 '23 16:03

lehn0058


1 Answers

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.

like image 94
lehn0058 Avatar answered Mar 16 '23 05:03

lehn0058