3

I am trying to display a user's daily step count in an Apple Watch complication. I setup my class by calling HKHealthStore's requestAuthorizationToShareTypes method and the complication displays steps correctly when it is first added to the watch face. However, the refresh is never successful while making a health kit query. I am suspicious that it is something to do with HealthKit permissions because the HKSampleQuery's completion handler doesn't get called. If I just comment out the health kit query, then my code refreshes as expected. Does anyone know what I may be missing? Or if complication background refresh are not allowed to access HealthKit?

Here is the code block that does work:

/// Provide the entry that should currently be displayed.
/// If you pass back nil, we will conclude you have no content loaded and will stop talking to you until you next call -reloadTimelineForComplication:.
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void) {

        let calendar = NSCalendar.currentCalendar()
        let now = NSDate()
        var startDate: NSDate? = nil
        var interval: NSTimeInterval = 0
        let endDate = NSDate()

        calendar.rangeOfUnit(NSCalendarUnit.Day, startDate: &startDate, interval: &interval, forDate: now)

        // Show dummy step data...
        let timelineEntry = self.buildTimelineEntry(complication, stepCount: 10, currentDateInterval: NSDate())
        handler(timelineEntry)
}

Here is the code block that does not work. The update in the error case doesn't even get called:

/// Provide the entry that should currently be displayed.
/// If you pass back nil, we will conclude you have no content loaded and will stop talking to you until you next call -reloadTimelineForComplication:.
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void) {

        let calendar = NSCalendar.currentCalendar()
        let now = NSDate()
        var startDate: NSDate? = nil
        var interval: NSTimeInterval = 0
        let endDate = NSDate()

        calendar.rangeOfUnit(NSCalendarUnit.Day, startDate: &startDate, interval: &interval, forDate: now)

        let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.StrictStartDate)
        let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: true)
        let stepSampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)!
        let sampleQuery = HKSampleQuery(sampleType: stepSampleType, predicate: predicate, limit: 0, sortDescriptors: [sortDescriptor]) { (sampleQuery, results, error ) -> Void in

            if error != nil {
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    let timelineEntry = self.buildTimelineEntry(complication, stepCount: 10, currentDateInterval: NSDate())
                    handler(timelineEntry)
                })

                return
            }

            self.currentSteps = [HKQuantitySample]()

            if results != nil {
                self.currentSteps = results as! [HKQuantitySample]
            }

            let countUnit = HKUnit(fromString: "count")
            var stepCount = 0.0
            var currentDate = now
            for result in self.currentSteps {
                stepCount += result.quantity.doubleValueForUnit(countUnit)
                currentDate = result.endDate
            }

            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                let timelineEntry = self.buildTimelineEntry(complication, stepCount: stepCount, currentDateInterval: currentDate)
                handler(timelineEntry)
            })
        }

        self.healthStore.executeQuery(sampleQuery)
}
Sam Spencer
  • 8,492
  • 12
  • 76
  • 133
lehn0058
  • 19,977
  • 15
  • 69
  • 109

1 Answers1

3

Attempting to asynchronously fetch (HealthKit) data within the complication controller will be unreliable.

In addition, trying to fetch or compute within the complication controller will needlessly use up the execution time budget that is allotted to your complication.

Apple recommends that you fetch the data and cache it before the complication data source needs it.

The job of your data source class is to provide ClockKit with any requested data as quickly as possible. The implementations of your data source methods should be minimal. Do not use your data source methods to fetch data from the network, compute values, or do anything that might delay the delivery of that data. If you need to fetch or compute the data for your complication, do it in your iOS app or in other parts of your WatchKit extension, and cache the data in a place where your complication data source can access it. The only thing your data source methods should do is take the cached data and put it into the format that ClockKit requires.

  • That makes it sound like trying to display health kit data is trouble then. I can't can the iPhone prepare the values because the data is on the watch and not necessarily synced to the iPhone yet. I also can't make the apple watch extension prepare the data since the app likely won't be launched and there isn't a good way to schedule a reoccurring task. The only place that really makes sense is in the complication. – lehn0058 Jan 02 '16 at 18:32
  • That would explain what I am seeing though if the system decided the complication was taking too long to return its data and was killing the process. – lehn0058 Jan 02 '16 at 18:46
  • I think the approach is to perform the fetch on the phone, then use `extendTimelineForComplication` or `transferCurrentComplicationUserInfo`. –  Jan 02 '16 at 18:52
  • I'm not sure if it makes sense for that to be the preferred method of refreshing. If it where, then why (hypothetically) have the getNextRequestedUpdateDateWithHandler method at all? It kind of seems like this method is pointless if the iPhone is expected to tell the complication when to update. Or is the getNextRequestedUpdateDateWithHandle method telling the complication when to kick off the request on the iPhone? – lehn0058 Jan 03 '16 at 01:05
  • @lehn0058 That's an approach for when the data is asynchronously fetched, apart from the complication controller. You're providing data to the extension, then manually extending the timeline. If the data was already cached, the complication controller could simply use the available data when a scheduled update occurred, and the phone wouldn't be involved. –  Jan 03 '16 at 01:29
  • That I can agree with. The problem I now face is how to get time to cache the data. If the phone isn't involved, the only way I can think of to get an event to calculate the cached data is by the scheduled complication method, since you can't schedule the watch app to wake up at certain intervals, and that seems to be the root of my problem. – lehn0058 Jan 03 '16 at 01:33
  • @lehn0058 Here's [an example](https://github.com/cchestnut91/DrinkKeeper) of how a complication controller uses its scheduled update to retrieve cached data from a shared data manager which gets HealthKit updates from an iOS app. Phone fetches data, manager caches data, complication uses cached data. –  Jan 03 '16 at 02:09
  • Yeah, I think this is similar to the new route I am going down. It looks like its not possible for the watch to currently handle this on its own. I need an observer query on the iPhone to listen for health kit updates. When the observer is triggered, then I can tell the complication that it has new data and should update. Thanks for your help working through this issue! – lehn0058 Jan 03 '16 at 02:12