基于HealthKit:计算单日平均心率及中等心率区间运动时长
HealthKit单日平均心率计算与中等心率区间时长统计方案
你的现有代码仅生成了模拟心率样本,未实现从HealthKit数据库查询真实数据、计算平均值及统计区间时长的核心逻辑。以下是完整实现方案:
一、前置准备:权限申请
首先必须请求读取心率数据的权限,否则无法访问HealthKit中的数据:
import HealthKit class HealthKitManager { let healthStore = HKHealthStore() // 请求心率读取权限 func requestHeartRateAuthorization(completion: @escaping (Bool, Error?) -> Void) { guard HKHealthStore.isHealthDataAvailable() else { completion(false, NSError(domain: "HealthKit", code: -1, userInfo: [NSLocalizedDescriptionKey: "HealthKit不可用"])) return } let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)! let readTypes: Set<HKObjectType> = [heartRateType] healthStore.requestAuthorization(toShare: nil, read: readTypes) { success, error in completion(success, error) } } }
二、计算最大心率
中等心率区间基于最大心率的64%-76%,通常采用220 - 年龄的通用公式,也可读取用户在健康APP中设置的信息计算更精准值:
// 计算最大心率(示例:基于年龄) func calculateMaxHeartRate(age: Int) -> Double { return 220 - Double(age) } // 从HealthKit获取用户年龄(可选) func getUserAge(completion: @escaping (Int?) -> Void) { let birthdayType = HKObjectType.characteristicType(forIdentifier: .dateOfBirth)! do { let birthday = try healthStore.dateOfBirthComponents() let age = Calendar.current.dateComponents([.year], from: birthday.date!, to: Date()).year! completion(age) } catch { completion(nil) } }
三、实现单日平均心率查询
使用HKStatisticsQuery直接计算指定日期内的心率平均值:
func getDailyAverageHeartRate(forDate date: Date, completion: @escaping (Double?, Error?) -> Void) { let cal = Calendar.current let startDate = cal.startOfDay(for: date) let endDate = cal.date(byAdding: .day, value: 1, to: startDate)! let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)! let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) // 创建统计查询,计算平均值 let query = HKStatisticsQuery(quantityType: heartRateType, quantitySamplePredicate: predicate, options: .average) { _, result, error in guard let result = result, let averageQuantity = result.averageQuantity() else { completion(nil, error) return } // 将心率转换为count/min单位 let averageHeartRate = averageQuantity.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute())) completion(averageHeartRate, nil) } healthStore.execute(query) }
四、统计中等心率区间时长(含30/60分钟维度)
先获取当日所有心率样本,遍历判断连续处于区间内的时间段,同时统计总时长、单次持续≥30分钟的累计时长、单次持续≥60分钟的累计时长:
struct HeartRateIntervalStats { let totalMediumZoneDuration: TimeInterval // 中等区间总时长 let durationWith30MinPlusSessions: TimeInterval // 单次≥30分钟的累计时长 let durationWith60MinPlusSessions: TimeInterval // 单次≥60分钟的累计时长 } func getMediumHeartRateZoneStats(forDate date: Date, maxHeartRate: Double, completion: @escaping (HeartRateIntervalStats?, Error?) -> Void) { let cal = Calendar.current let startDate = cal.startOfDay(for: date) let endDate = cal.date(byAdding: .day, value: 1, to: startDate)! let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)! let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) // 获取所有心率样本 let query = HKSampleQuery(sampleType: heartRateType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)]) { _, samples, error in guard let samples = samples as? [HKQuantitySample], error == nil else { completion(nil, error) return } let lowerBound = maxHeartRate * 0.64 let upperBound = maxHeartRate * 0.76 var totalDuration: TimeInterval = 0 var currentSessionStart: Date? var thirtyMinPlusTotal: TimeInterval = 0 var sixtyMinPlusTotal: TimeInterval = 0 // 遍历样本,计算连续处于中等区间的时长 for sample in samples { let heartRate = sample.quantity.doubleValue(for: HKUnit.count().unitDivided(by: HKUnit.minute())) let isInMediumZone = heartRate >= lowerBound && heartRate <= upperBound let sampleDuration = sample.endDate.timeIntervalSince(sample.startDate) if isInMediumZone { if currentSessionStart == nil { currentSessionStart = sample.startDate } totalDuration += sampleDuration } else { // 退出中等区间,计算当前会话时长 if let sessionStart = currentSessionStart { let sessionDuration = sample.startDate.timeIntervalSince(sessionStart) if sessionDuration >= 30 * 60 { thirtyMinPlusTotal += sessionDuration if sessionDuration >= 60 * 60 { sixtyMinPlusTotal += sessionDuration } } currentSessionStart = nil } } } // 处理最后一个未结束的会话 if let sessionStart = currentSessionStart { let sessionDuration = endDate.timeIntervalSince(sessionStart) if sessionDuration >= 30 * 60 { thirtyMinPlusTotal += sessionDuration if sessionDuration >= 60 * 60 { sixtyMinPlusTotal += sessionDuration } } } let stats = HeartRateIntervalStats(totalMediumZoneDuration: totalDuration, durationWith30MinPlusSessions: thirtyMinPlusTotal, durationWith60MinPlusSessions: sixtyMinPlusTotal) completion(stats, nil) } healthStore.execute(query) }
使用示例
let healthManager = HealthKitManager() // 1. 请求权限 healthManager.requestHeartRateAuthorization { success, error in guard success else { return } // 2. 获取用户年龄,计算最大心率 healthManager.getUserAge { age in guard let age = age else { return } let maxHR = healthManager.calculateMaxHeartRate(age: age) let targetDate = Date() // 目标日期 // 3. 获取单日平均心率 healthManager.getDailyAverageHeartRate(forDate: targetDate) { avgHR, error in if let avgHR = avgHR { print("当日平均心率:\(avgHR) bpm") } } // 4. 获取中等心率区间统计 healthManager.getMediumHeartRateZoneStats(forDate: targetDate, maxHeartRate: maxHR) { stats, error in if let stats = stats { print("中等区间总时长:\(stats.totalMediumZoneDuration/60) 分钟") print("单次≥30分钟累计时长:\(stats.durationWith30MinPlusSessions/60) 分钟") print("单次≥60分钟累计时长:\(stats.durationWith60MinPlusSessions/60) 分钟") } } } }
关键说明
- 心率样本连续性:实际场景中,若心率样本间隔超过阈值(如1分钟),可视为会话中断,需调整代码中的连续性判断逻辑。
- 单位一致性:全程使用
count/min单位处理心率值,避免单位不匹配导致计算错误。 - 错误处理:实际使用中需完善权限未授权、无数据等分支的错误提示逻辑。
内容的提问来源于stack exchange,提问作者AaoIi
相关产品推荐
相关产品推荐

