Highlight
iOS 27 和 watchOS 27 将心率区间(Heart Rate Zones)和骑行功率区间(Cycling Power Zones)直接集成到 HealthKit,开发者无需自己计算区间阈值,也无需维护区间逻辑,只需调用新 API 即可读取系统计算好的区间数据、接收实时区间变化通知、或提供自定义区间配置。
核心内容
以前怎么做:每个健身 App 都要自己算
做一款健身 App,心率区间是绕不开的功能。用户跑完步想看自己在每个强度区间待了多久,开发者的选择很有限:
- 自己维护一套区间计算公式,根据用户年龄和静息心率算 5 个区间阈值
- 自己比对每条心率样本落在哪个区间,累加时间
- 自己处理多设备同步,保证 iPhone 和 Apple Watch 上的区间数据一致
- 不同 App 的算法不同,用户换 App 后区间定义全变
更麻烦的是,骑行功率区间(Cycling Power Zones)基于功能性阈值功率(FTP),计算逻辑和心率区间完全不同。一个 App 要同时支持两种区间,代码复杂度直接翻倍。
Apple 做了什么
HealthKit 在 iOS 27 和 watchOS 27 中新增了 Workout Zone 体系,把心率区间和骑行功率区间的计算、存储、同步全部接管。
系统会根据用户的年龄、静息心率自动计算心率区间阈值。用户也可以在「健康」App 中手动调整这些阈值。所有区间配置通过 HealthKit 跨设备同步,换 App 也不变。
开发者只需要请求相应的权限,就能读取系统已经算好的区间数据,或者提供自己的自定义区间。
这解决了什么
对开发者来说,省去了维护区间算法和跨设备同步的代码。对用户来说,不同健身 App 看到的心率区间定义一致,数据可以无缝衔接。
详细内容
读取已完成运动的区间数据
(03:54)
运动结束后,从 HKWorkout 或 HKWorkoutActivity 上读取区间数据:
if let heartRateZoneGroup = workout.zoneGroupsByType?[HKQuantityType(.heartRate)] {
let zones = ZoneDisplayData(
zoneCount: heartRateZoneGroup.configuration.zones.count,
currentZoneIndex: nil,
durations: heartRateZoneGroup.zoneDurations.map(\.duration)
)
}
关键点:
zoneGroupsByType是一个字典,用HKQuantityType作为 key。传入HKQuantityType(.heartRate)获取心率区间,传入骑行功率的 quantity type 就能获取功率区间- 返回的
HKWorkoutZoneGroup包含两个核心属性:configuration(区间配置)和zoneDurations(每个区间的累计时长) zoneDurations按区间阈值从小到大排序,可以直接用来画柱状图
HKWorkoutZoneConfiguration 的结构值得细看一下:
quantityType:这个配置对应的数据类型,心率或骑行功率source:区间阈值是怎么来的——系统自动计算、用户在设置中手动配置、还是 App 自定义提供zones:区间数组,每个区间有index、minimum、maximum三个属性。第一个区间没有下限,最后一个没有上限,保证所有可能的值都被覆盖
实时接收区间变化
(07:57)
运动过程中,HealthKit 会实时处理 incoming 的心率样本,判断当前落在哪个区间。当区间变化时(比如从 Zone 2 跳到 Zone 3),通过 delegate 通知 App:
func workoutBuilder(
_ workoutBuilder: HKLiveWorkoutBuilder,
didUpdateWorkoutZone zoneUpdate: HKLiveWorkoutZoneUpdate
) {
guard let zoneGroup = zoneUpdate.zoneGroup else {
return
}
if let currentIndex = zoneUpdate.currentZoneDuration?.zone.index {
let data = ZoneDisplayData(
zoneCount: zoneGroup.configuration.zones.count,
currentZoneIndex: currentIndex,
durations: zoneGroup.zoneDurations.map(\.duration)
)
Task { @MainActor in
self.heartRateZones = data
}
}
}
关键点:
- 实现
HKLiveWorkoutBuilderDelegate协议的didUpdateWorkoutZone方法 zoneUpdate.currentZoneDuration包含当前区间和累计时长zoneUpdate.zoneGroup包含完整的区间配置和所有区间的累计时长- 只有在当前区间真正变化时才会收到通知,不是每次样本都发
- 用
@MainActor把 UI 更新切回主线程
这个机制让实时教练功能变得很简单。App 可以在用户偏离目标区间时发出提醒,或者高亮显示当前区间。
检查用户的偏好区间配置
(09:19)
开始区间运动前,先确认用户是否已经设置了偏好区间配置:
if try await builder.zoneConfiguration(for: HKQuantityType(.heartRate)) == nil {
// 用户没有设置偏好区间,需要提供自定义配置
}
关键点:
- 在
HKWorkoutBuilder或HKHealthStore上都可以查询 - 返回
nil表示用户没有在「健康」App 中配置过区间阈值 - 偏好区间是跨设备同步的,在一个设备上设置,所有设备生效
创建自定义区间配置
(09:24)
如果用户没有偏好配置,或者你的 App 有专有的区间模型,可以提供自定义配置:
let defaultHeartRateZoneThresholds = [91.0, 114.0, 136.0, 158.0]
let bpmUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
let boundaries = defaultHeartRateZoneThresholds.map {
HKQuantity(unit: bpmUnit, doubleValue: $0)
}
let heartRate = HKQuantityType(.heartRate)
let defaultConfiguration = try HKWorkoutZoneConfiguration(
quantityType: heartRate,
zoneBoundaries: boundaries
)
try await builder.setCustomZoneConfiguration(defaultConfiguration, for: heartRate)
(10:03)
设置完成后开始数据收集:
let startDate = Date()
try await builder.beginCollection(at: startDate)
关键点:
- 区间边界用
HKQuantity表示,单位必须与 quantity type 兼容。心率用count/minute,骑行功率用watt - HealthKit 根据边界自动创建区间,第一个区间从 0 开始,最后一个无上界
- 必须提供 3 到 9 个区间
- 自定义配置必须在调用
beginCollection之前设置 - 自定义配置只保存在当前运动的上下文中,App 需要自己负责保存和同步
跨运动比较时的注意事项
(10:43)
不同运动的区间数量可能不同。系统默认心率区间是 5 个,骑行功率默认是 6 个,但有些训练平台用 5 个、7 个或 8 个。
直接比较「Zone 3 待了多久」在不同区间数量的运动之间没有意义。5 区运动的 Zone 3 和 7 区运动的 Zone 3 覆盖的心率范围完全不同。
正确的做法是从原始样本重新计算:取出运动中的原始心率样本,按目标区间数量重新分桶。HealthKit 已经帮你算好了每条样本落在哪个区间,但跨运动比较时需要归一化处理。
核心启发
1. 运动后智能总结页
- 做什么:运动结束后展示一张区间分布图,用颜色区分 5 个心率区间,直观显示用户在哪段强度待得最久
- 为什么值得做:HealthKit 已经算好了
zoneDurations,你只需读取数据、画个柱状图,几分钟就能实现 - 怎么开始:运动结束后从
HKWorkout.zoneGroupsByType读取数据,用zoneDurations.map(\.duration)拿到每个区间的时长数组
2. 实时区间教练
- 做什么:运动过程中高亮显示当前心率区间,当用户偏离目标区间时发出触觉反馈或语音提醒
- 为什么值得做:
didUpdateWorkoutZone只在区间变化时触发,不会频繁打扰用户,同时又能及时提醒 - 怎么开始:实现
HKLiveWorkoutBuilderDelegate,在didUpdateWorkoutZone中比对当前区间和目标区间,偏离时触发提醒
3. 长期训练负荷分析
- 做什么:按周/月汇总用户在各区间累计的时间,生成训练负荷趋势图
- 为什么值得做:区间数据比原始心率更有训练意义,Zone 1-2 代表恢复,Zone 4-5 代表高强度,长期趋势能反映训练是否合理
- 怎么开始:读取历史
HKWorkout的zoneGroupsByType,累加各区间时长,用 Swift Charts 画趋势图
4. 支持骑行功率区间
- 做什么:为骑行 App 添加功率区间支持,结构和心率区间完全一致
- 为什么值得做:骑行用户普遍使用功率训练,但功率区间的 FTP 计算一直是个门槛。HealthKit 接管后,开发者直接读取即可
- 怎么开始:把 quantity type 从
HKQuantityType(.heartRate)换成骑行功率对应的 type,其余代码结构不变
5. 自定义训练平台的区间模型
- 做什么:如果你的训练平台有专有的区间算法(比如基于 HRV 或乳酸阈值的模型),可以用自定义区间配置覆盖系统默认值
- 为什么值得做:既保留了 HealthKit 的区间基础设施(实时通知、跨运动存储),又能体现平台的差异化算法
- 怎么开始:计算好你的区间边界阈值,创建
HKWorkoutZoneConfiguration,在beginCollection前通过setCustomZoneConfiguration设置
关联 Session
- Session 303: Build a great camera experience — 健身 App 常需结合相机功能记录训练过程
- Session 304: Capture the perfect photo — 运动场景下的照片捕捉优化
- Session 223: Live Activities — 把实时心率区间显示在锁屏和灵动岛上
- Session 262: Swift — HealthKit API 基于 Swift 并发模型设计,大量使用了
async/await - Session 274: SwiftData — 将历史区间数据持久化到本地数据库,支持离线查看训练记录
评论
GitHub Issues · utterances