WWDC Quick Look 💓 By SwiftGGTeam
Deliver workout insights with HealthKit workout zones

Deliver workout insights with HealthKit workout zones

观看原视频

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

运动结束后,从 HKWorkoutHKWorkoutActivity 上读取区间数据:

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:区间数组,每个区间有 indexminimummaximum 三个属性。第一个区间没有下限,最后一个没有上限,保证所有可能的值都被覆盖

实时接收区间变化

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 {
    // 用户没有设置偏好区间,需要提供自定义配置
}

关键点:

  • HKWorkoutBuilderHKHealthStore 上都可以查询
  • 返回 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 代表高强度,长期趋势能反映训练是否合理
  • 怎么开始:读取历史 HKWorkoutzoneGroupsByType,累加各区间时长,用 Swift Charts 画趋势图

4. 支持骑行功率区间

  • 做什么:为骑行 App 添加功率区间支持,结构和心率区间完全一致
  • 为什么值得做:骑行用户普遍使用功率训练,但功率区间的 FTP 计算一直是个门槛。HealthKit 接管后,开发者直接读取即可
  • 怎么开始:把 quantity type 从 HKQuantityType(.heartRate) 换成骑行功率对应的 type,其余代码结构不变

5. 自定义训练平台的区间模型

  • 做什么:如果你的训练平台有专有的区间算法(比如基于 HRV 或乳酸阈值的模型),可以用自定义区间配置覆盖系统默认值
  • 为什么值得做:既保留了 HealthKit 的区间基础设施(实时通知、跨运动存储),又能体现平台的差异化算法
  • 怎么开始:计算好你的区间边界阈值,创建 HKWorkoutZoneConfiguration,在 beginCollection 前通过 setCustomZoneConfiguration 设置

关联 Session

评论

GitHub Issues · utterances