WidgetKit小组件间歇性无法获取HealthKit卡路里消耗数据求助
解决HealthKit数据间歇性丢失与WidgetKit小组件异常问题
一、代码核心优化建议
1. 替换强制解包,添加完整错误处理
代码中大量使用!强制解包,一旦HealthKit返回的数据格式不符合预期(比如样本来源字段缺失、设备信息异常),会直接触发崩溃或静默失败,导致计算出的总和归零。必须全部替换为可选绑定,并添加错误分支处理:
func getHealthKitDataAggregate(completion: @escaping ((Double, Int) -> Void)) { let startOfDay = Calendar.current.startOfDay(for: Date()) guard let activeEnergyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned), let basalEnergyType = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned) else { completion(0, 0) return } let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: .now, options: .strictStartDate) // 查询主动消耗卡路里 let activeQuery = HKSampleQuery(sampleType: activeEnergyType, predicate: predicate, limit: 0, sortDescriptors: nil) { _, results, error in if let error = error { os_log("主动消耗卡路里查询失败: %@", log: .default, type: .error, error.localizedDescription) completion(0, self.getConsumedCalories()) return } let activeSum = self.calculateSum(from: results, targetBundleId: "com.apple.health", deviceNameContains: "Apple Watch") // 查询基础消耗卡路里 let basalQuery = HKSampleQuery(sampleType: basalEnergyType, predicate: predicate, limit: 0, sortDescriptors: nil) { _, results, error in if let error = error { os_log("基础消耗卡路里查询失败: %@", log: .default, type: .error, error.localizedDescription) completion(activeSum, self.getConsumedCalories()) return } let basalSum = self.calculateSum(from: results, targetBundleId: "com.apple.health", deviceNameContains: "Apple Watch") completion(activeSum + basalSum, self.getConsumedCalories()) } self.healthStore.execute(basalQuery) } self.healthStore.execute(activeQuery) } // 提取重复过滤求和逻辑为辅助函数 private func calculateSum(from results: [HKSample]?, targetBundleId: String, deviceNameContains: String) -> Double { guard let samples = results as? [HKQuantitySample] else { return 0 } let filteredSamples = samples.filter { sample in guard let bundleId = sample.sourceRevision.source.bundleIdentifier, bundleId.contains(targetBundleId), let device = sample.device, let deviceName = device.name, deviceName.contains(deviceNameContains) else { return false } return true } return filteredSamples.reduce(0) { sum, sample in sum + sample.quantity.doubleValue(for: HKUnit.kilocalorie()) } } // 单独提取UserDefaults读取逻辑 private func getConsumedCalories() -> Int { guard let userDefaults = UserDefaults(suiteName: "group.com.johncorser.fit.prefs"), let calorieStr = userDefaults.string(forKey: "group.com.johncorser.fit.prefs.consumedCalories"), let calories = Int(calorieStr) else { return 0 } return calories }
2. 使用HKStatisticsQuery替代HKSampleQuery(更高效可靠)
HKSampleQuery会返回所有原始样本,遍历过滤效率低,且在后台刷新时容易因超时被系统终止。改用专门用于统计的HKStatisticsQuery,直接获取总和,减少数据处理量:
func getHealthKitDataAggregate(completion: @escaping ((Double, Int) -> Void)) { let startOfDay = Calendar.current.startOfDay(for: Date()) guard let activeEnergyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned), let basalEnergyType = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned) else { completion(0, getConsumedCalories()) return } let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: .now, options: .strictStartDate) let calorieUnit = HKUnit.kilocalorie() // 通用统计查询函数 func fetchSum(for type: HKQuantityType, completion: @escaping (Double) -> Void) { let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in if let error = error { os_log("%@统计查询失败: %@", log: .default, type: .error, type.rawValue, error.localizedDescription) completion(0) return } guard let sumQuantity = result?.sumQuantity() else { os_log("%@无统计数据", log: .default, type: .info, type.rawValue) completion(0) return } completion(sumQuantity.doubleValue(for: calorieUnit)) } healthStore.execute(query) } // 并行查询两类卡路里数据 let group = DispatchGroup() var activeSum = 0.0 var basalSum = 0.0 group.enter() fetchSum(for: activeEnergyType) { sum in activeSum = sum group.leave() } group.enter() fetchSum(for: basalEnergyType) { sum in basalSum = sum group.leave() } group.notify(queue: .main) { completion(activeSum + basalSum, self.getConsumedCalories()) } }
3. 改用Async/Await简化嵌套回调(iOS 15+)
如果项目支持iOS 15及以上,用Async/Await替代嵌套回调,代码更易读且可靠性更高:
func getHealthKitDataAggregate() async -> (Double, Int) { let startOfDay = Calendar.current.startOfDay(for: Date()) guard let activeEnergyType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned), let basalEnergyType = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned) else { return (0, getConsumedCalories()) } let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: .now, options: .strictStartDate) let calorieUnit = HKUnit.kilocalorie() func fetchSum(for type: HKQuantityType) async -> Double { return await withCheckedThrowingContinuation { continuation in let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in if let error = error { os_log("%@统计查询失败: %@", log: .default, type: .error, type.rawValue, error.localizedDescription) continuation.resume(returning: 0) return } guard let sumQuantity = result?.sumQuantity() else { os_log("%@无统计数据", log: .default, type: .info, type.rawValue) continuation.resume(returning: 0) return } continuation.resume(returning: sumQuantity.doubleValue(for: calorieUnit)) } healthStore.execute(query) } } let activeSum = await fetchSum(for: activeEnergyType) let basalSum = await fetchSum(for: basalEnergyType) return (activeSum + basalSum, getConsumedCalories()) }
二、调试与日志方案(针对间歇性问题)
由于无法在问题发生时连接调试器,必须通过日志追踪问题:
1. 使用OSLog记录关键事件
在代码中添加详细日志,覆盖查询开始、结束、错误、计算结果等节点:
import os.log private let logger = OSLog(subsystem: "com.johncorser.fit.widget", category: "HealthKit") // 查询开始时记录 os_log("开始查询当日卡路里消耗数据", log: logger, type: .info) // 获取到数据后记录 os_log("主动消耗: %.2f kcal,基础消耗: %.2f kcal", log: logger, type: .info, activeSum, basalSum) // 错误发生时记录 os_log("HealthKit查询错误: %@", log: logger, type: .error, error.localizedDescription)
2. 在Console.app中查看日志
- 打开Console.app,选择你的测试设备
- 在搜索栏输入
subsystem:com.johncorser.fit.widget过滤目标日志 - 观察问题发生时的日志,定位是查询失败、无数据还是处理逻辑异常
3. 持久化关键数据到共享UserDefaults
将每次计算的总和与时间戳保存到共享容器,方便事后回溯数据异常:
func recordHealthKitData(sum: Double, timestamp: Date = Date()) { guard let userDefaults = UserDefaults(suiteName: "group.com.johncorser.fit.prefs") else { return } userDefaults.set(sum, forKey: "lastHealthKitCalorieSum") userDefaults.set(timestamp.timeIntervalSince1970, forKey: "lastHealthKitQueryTime") }
三、关键注意事项
- WidgetKit后台刷新限制:小组件后台刷新时间通常仅有几秒,必须确保HealthKit查询足够高效,避免耗时操作
- HealthKit权限验证:在小组件启动时重新验证HealthKit权限,防止后台访问权限被用户撤销
- 缓存 fallback 策略:当HealthKit查询失败时,使用上一次成功获取的缓存数据,避免小组件显示异常
- 避免主线程阻塞:所有HealthKit操作必须在后台队列执行,不要阻塞主线程
内容的提问来源于stack exchange,提问作者johncorser
相关产品推荐
相关产品推荐

