So I am a beginner to iOS development and especially developing iOS widgets.
I wrote this widget (code below) for my app which is mostly working how I expect. The app is a calorie tracking app, and the widget shows how many calories you've burned vs how many you've consumed today, then does the subtraction to show your caloric deficit.
The problem that I'm having is, as day goes on, and the widget keeps refreshing to update itself, it occasional fails entirely to pull the caloric burn information from HealthKit. This leaves the widget in a bizarre broken state, which resolves itself if ignored for long enough.
(don't worry that it's 100 consumed calories instead of 500 like the previous screenshot, that's not a problem, I just changed it between screenshots. The consumed calories portion of this works properly. It's only the HealthKit part that zeroes itself out unexpectedly)
I have no clue why this happening and I don't know how I might update the implementation to be more reliable. Part of this is that I don't fully understand swift and how to manage the callback hell that comes with nesting HealthKit's completion handlers.
It takes a while before this happens, and you have to catch it at the right time otherwise it fixes itself the next time the widget refreshes. Therefore I haven't been able to attach a debugger while it was happening to figure out what happened.
Any advice would be helpful - do not underestimate how little I know about developing for the iOS ecosystem. Code feedback, debugging / logging tools, anything can be helpful in tracking down why this happens. I appreciate your advice.
Here is the relevant code snippet for the widget:
func getHealthKitDataAggregate(completion: @escaping ((Double, Int) -> Void)) -> Void {
let oneDayAgo = Calendar.current.startOfDay(for: Date())
let activeEnergySampleType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.activeEnergyBurned)!
let basalEnergySampleType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.basalEnergyBurned)!
let predicate = HKQuery.predicateForSamples(withStart: oneDayAgo, end: .now, options: HKQueryOptions.strictStartDate)
let query = HKSampleQuery(sampleType: activeEnergySampleType, predicate: predicate, limit: 0, sortDescriptors: nil) {
_, results, _ in
let outputs: [[String: Any]] = generateOutput(results: results)!
var filtered = outputs.filter({ ($0["sourceBundleId"] as! String).contains("com.apple.health") })
filtered = filtered.filter({ (($0["device"] as! [String: Any])["name"] as! String).contains("Apple Watch") })
let numbers = filtered.map({ $0["value"] as! Double })
let activeSum = numbers.reduce(0, { x, y in
x + y
})
let query = HKSampleQuery(sampleType: basalEnergySampleType, predicate: predicate, limit: 0, sortDescriptors: nil) {
_, results, _ in
let outputs: [[String: Any]] = generateOutput(results: results)!
var filtered = outputs.filter({ ($0["sourceBundleId"] as! String).contains("com.apple.health") })
filtered = filtered.filter({ (($0["device"] as! [String: Any])["name"] as! String).contains("Apple Watch") })
let numbers = filtered.map({ $0["value"] as! Double })
let basalSum = numbers.reduce(0, { x, y in
x + y
})
if let userDefaults = UserDefaults(suiteName: "group.com.johncorser.fit.prefs") {
let consumedCalories = Int(userDefaults.string(forKey: "group.com.johncorser.fit.prefs.consumedCalories") ?? "0")
completion(basalSum + activeSum, consumedCalories ?? 0) }
else {
completion(basalSum + activeSum, 0)
}
}
healthStore.execute(query)
}
healthStore.execute(query)
}
The issue is basalSum + activeSum
is intermittently resolving to 0 in the case of background widget refreshes, even when it should have a number there.
If the phone is locked, it's not possible to get HealthKit data, only if the phone is unlocked. However, the widget's timeline's will try to update even if the phone is locked, leading to errors from HealthKit that the above code sample interprets as zeroes.
As far as I can tell, there is no way to control this behavior. However, I found one developer that uses UserDefaults to cache the last known value, and displays the value from the cache when HealthKit encounters this error. See How can I keep the existing entry on WidgetKit refresh?
This is the workaround that I've gone with. It's not perfect, but given the constraints it'll have to do.