You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 21:52:05