iOS 27 中的 HealthKit:训练区间与新增类型
多年来,训练心率区间在 HealthKit 中一直只是一个非官方的存在:每一款绘制五区间柱状图的跑步和骑行应用都得自己计算这些区间——拉取原始的 HKQuantitySample 心率读数,按硬编码或用户手动输入的阈值分桶,再逐一累加每个桶内的时长。iOS 27 终结了这种手工活。区间成为系统生成的一种结构化 HealthKit 类型,用户可以在「健康」设置中编辑,而您的应用读回时,各区间内的停留时长已经统计完毕。同一版本还新增了两种生殖健康分类类型——menopausalState 与 bleedingAfterMenopause,填补了 HealthKit 周期追踪模型中的一处空白。1
iOS 27 的这批新增贯穿着两条主线,二者形态相通。训练区间这条线把应用过去自行掌控的计算搬进了框架,于是同一个人的区间定义在每一个请求它的应用里都保持一致。新类型这条线,则为 HealthKit 此前无法表达的人生阶段提供了带类型的分类样本。两者都沿用了 HealthKit 既有的语法:用量值类型表示测量值,用分类类型表示事件与状态。本文将对照 Apple 官方文档逐一梳理,并延续本系列一贯的视角:一个已经接入 HealthKit 的应用,要获得每项能力需要补上什么。
太长不看
HKWorkoutZoneConfiguration为某个量值类型定义一整套完整的区间。系统会根据用户的健康指标自动生成区间,用户可在「健康」设置中编辑,应用也可为特定训练提供自定义区间。2HKWorkoutZoneGroup将区间配置与其各区间停留时长数据配对在一起。您从已完成的训练实例中读回它,便可直接拿到框架已经算好的每区间时长,而无需自己对心率样本分桶。3HKHealthStore上的preferredWorkoutZoneConfiguration(for:)返回用户对某个量值类型的首选区间配置,让您的图表与用户在其他各处看到的阈值保持一致。4menopausalState配合HKCategoryValueMenopausalState枚举,记录某一时间点的绝经状态;样本的起始日期与结束日期必须完全相同,否则保存会失败。56bleedingAfterMenopause以一个带强度值的时间区间记录绝经后出血,在临床上与月经流量明确区分开来。7
训练区间一向得靠你自己算
iOS 27 之前,想要支持心率区间的训练应用得把整套活儿都干完。它要查询训练时间段内的心率 HKQuantitySample 数据,决定区间边界落在哪里(220 减年龄、乳酸阈值百分比,或用户在设置界面里手动输入的数值),按顺序遍历样本,再累加每个区段内的秒数。每款应用都自定一套边界算法,于是同一个人用两款应用跑步,会看到两个不同的「区间 3」。没有任何区间数据存在 HealthKit 里,所以这些数据既不会同步,也无法在应用间通用。
iOS 27 引入了 HKWorkoutZoneConfiguration——一个为某个量值类型定义一整套完整区间的结构。2 该配置携带一个有序的区间数组,并标明它所适用的量值类型。真正关键的变化在于区间的来源。Apple 官方文档指出:系统会根据用户的健康指标自动生成区间;用户也可以在「健康」设置中手动配置区间;应用还可以为特定训练提供自定义区间。2 区间定义不再是每款应用的私有细节,而成为框架掌管的共享状态。
import HealthKit
let heartRateType = HKQuantityType(.heartRate)
// A custom configuration an app supplies for one workout.
// makeIntervalZones is your own builder; construct the configuration
// from the ordered zones your workout uses, per HKWorkoutZoneConfiguration's
// declaration. The configuration identifies the quantity type the zones apply to.
let configuration: HKWorkoutZoneConfiguration = makeIntervalZones(for: heartRateType)
与之配套的结构是 HKWorkoutZoneGroup,它包含某个量值类型的区间配置与各区间停留时长数据。3 区间组把一个 HKWorkoutZoneConfiguration 与每区间的时长数据结合在一起,因此读取一个组,您便同时拿到了边界以及用户在每个区间内停留的时长。Apple 官方文档介绍了如何从已完成的训练实例中访问区间组,以取回已结束训练的区间数据;也可从实时训练来源获取活跃训练过程中的实时区间信息。3 分桶由框架完成,您的代码只管读取结果。
在第 207 场会议中,Apple 表示:训练区间集成进 HealthKit 后,每个区间内的时长会在训练过程中根据传入样本自动计算,因此应用直接从 zoneGroupsByType 读取已汇总好的时长,而无需自己对心率样本分桶。8
import HealthKit
func summarize(_ group: HKWorkoutZoneGroup) {
// The group bundles the configuration (the zone boundaries)
// with the time-in-zone data the framework already computed.
let configuration = group.configuration
for zone in group.zones {
// Render each zone's accumulated time. No sample-walking,
// no manual bucketing — the durations arrive pre-summed.
render(zone)
}
}
第三块把配置绑定到了用户身上,而非应用。preferredWorkoutZoneConfiguration(for:) 是 HKHealthStore 上的一个实例方法,返回某人对指定量值类型的首选区间配置:4
func preferredWorkoutZoneConfiguration(
for quantityType: HKQuantityType
) async throws -> HKWorkoutZoneConfiguration?
Apple 官方文档明确写出了返回值约定:该方法返回用户在「健康」设置中手动配置的区间;若用户未设置自定义值,则返回系统生成的区间;若用户根本没有为该量值类型配置过区间,则返回 nil。4 系统生成的区间会随用户健康指标的变化定期更新;用户手动设置的区间则保持不变,直到他们再次编辑。文档所述意图在于:让应用展示的区间信息与用户的偏好在每一次训练中保持一致。4 当您希望自己的「区间 3」与用户在其他各处看到的「区间 3」相吻合,而不是自创一套时,就该用这个方法。
import HealthKit
let store = HKHealthStore()
let heartRateType = HKQuantityType(.heartRate)
func loadPreferredZones() async throws -> HKWorkoutZoneConfiguration? {
// nil means the person has not configured zones for heart rate.
// Fall back to your own defaults only in that case.
try await store.preferredWorkoutZoneConfiguration(for: heartRateType)
}
这种不对称值得点明。preferredWorkoutZoneConfiguration(for:) 读取的是用户的固定偏好,从而在训练前、训练中、训练后都画出一致的图表。而您自己构建的自定义 HKWorkoutZoneConfiguration,则用于某次特定训练需要不同于用户默认值的区间——比如一场自带分段的结构化间歇训练。多数应用想要的是首选配置;自定义这条路,是留给训练结构本身要求专属区间的场合。
新增分类类型:menopausalState 与 bleedingAfterMenopause
HealthKit 的周期追踪模型能处理月经,却没有带类型的方式来记录一个人处于绝经过渡期的哪个阶段,也无法记录月经结束之后出现的出血。iOS 27 将两者都新增为分类类型,沿用了 iOS 26 HealthKit 一文在讲正念会话与睡眠时介绍过的同一套 HKCategorySample 模式。
menopausalState 是一个分类类型标识符,用于记录某人绝经状态的样本。5 每个样本都是一条时间点条目,而 Apple 官方文档施加了一条硬性约束:起始日期必须等于结束日期,保存一个两者不同的样本会产生错误。5 每个样本的值是 HKCategoryValueMenopausalState 枚举的一个 case,用于表示某个记录时间点的绝经状态。6 由于该枚举的 case 列表在 Apple 的说明中被略去了,请把这个枚举本身视作有效取值的权威来源,而不要去猜测 case 名称;文档所述的行为是:每个样本在某个具体日期存储这样一个 case。6
import HealthKit
let store = HKHealthStore()
let menopausalType = HKCategoryType(.menopausalState)
func record(state: HKCategoryValueMenopausalState, on date: Date) async throws {
// Point-in-time sample: start and end MUST be identical.
// A mismatched start/end is a save error, by documented design.
let sample = HKCategorySample(
type: menopausalType,
value: state.rawValue,
start: date,
end: date
)
try await store.save(sample)
}
时间点模型决定了您该如何解读这些数据。Apple 官方文档指出:应用可以读取一段时间内的多个样本,从中推导出更高层的解释——活跃期、过渡,或当前状态。6 单个样本声明的是某个特定状态在某个特定日期成立,而把样本串联成时间线的工作,框架交给了应用。Apple 把每条条目描述为一次状态变化、一次「某状态在该日期成立」的确认,或两者兼有,取决于应用如何解读这一系列样本。5
bleedingAfterMenopause 是一个分类类型标识符,用于记录绝经后出血的样本。7 它之所以单独成为一种类型,原因正是临床上的区分。绝经之后月经已经停止,因此绝经后出血既不是月经流量,也不是经间期出血;Apple 官方文档称它在临床上与二者都明确不同。7 与时间点式的绝经状态样本不同,bleedingAfterMenopause 样本表示的是一段出血区间,并存储一个强度值。7
import HealthKit
let store = HKHealthStore()
let bleedingType = HKCategoryType(.bleedingAfterMenopause)
// `intensityValue` is the raw value of the intensity enum Apple defines for
// the bleedingAfterMenopause type; confirm the concrete case set in its declaration.
func record(intensityValue: Int, from start: Date, to end: Date) async throws {
// An interval sample, not point-in-time: start and end differ,
// and the value carries the bleeding intensity.
let sample = HKCategorySample(
type: bleedingType,
value: intensityValue,
start: start,
end: end
)
try await store.save(sample)
}
这两种类型之间的分野,正映照了 iOS 26 那篇文章在时间点式与区间式分类样本之间画出的分野。menopausalState 把自己的时间窗压缩为单一瞬间,承载一个状态。bleedingAfterMenopause 则保留一个真实的 [start, end] 时间窗,承载一个强度。任选一种选错了形态,要么是保存错误,要么是一条毫无意义的记录——所以早在您写下一行查询代码之前,所选的类型就已经把数据的本质编码了进去。
接入路径
一个已经接入 HealthKit 的训练或健康应用,可以逐步加上 iOS 27 的这些新接口;其中没有任何一项需要重写 iOS 26 模式一文所讲的授权或样本保存管道。
- 删掉你的区间算法。 任何手工计算心率区间的应用,都应当对相关量值类型调用
preferredWorkoutZoneConfiguration(for:)并渲染返回的配置,仅在调用返回nil时才回退到自己的默认值。4 您的区间从此与用户在系统范围内看到的保持一致。 - 读
HKWorkoutZoneGroup,别再给样本分桶。 对于已完成训练的总结,拉取区间组并渲染其预先算好的各区间停留时长,而不要自己遍历原始心率样本去累加时长。3 - 仅当某次训练需要专属分段时才提供自定义
HKWorkoutZoneConfiguration。 带定制区间的结构化间歇训练正是自定义配置的适用场景;其余一律使用用户的首选配置。2 - 把新增的分类类型加进你的授权请求。 一个周期追踪应用要把
menopausalState与bleedingAfterMenopause加入其写入与读取集合,然后把menopausalState记录为时间点样本(起始等于结束),把bleedingAfterMenopause记录为带强度的区间样本。57 - 逐类型核对你的起始/结束日期。 绝经状态样本在起始与结束不同时会保存失败;出血样本是区间,本就要求两者不同。在交付保存逻辑之前,把时间窗的形态弄对。57
常见问题
iOS 27 里我还得自己计算心率区间吗?
不用,常见情况下不必。preferredWorkoutZoneConfiguration(for:) 返回用户对某个量值类型的首选区间配置(他们在「健康」设置中手动配置的区间、系统生成的区间,或者在他们尚未配置任何区间时返回 nil),而 HKWorkoutZoneGroup 携带框架已为某次训练算好的各区间停留时长数据。34 只有当某次特定训练需要不同于用户默认值的自定义分段时,您才自己计算区间,这时为那次训练构建一个 HKWorkoutZoneConfiguration。2
HKWorkoutZoneConfiguration 与 HKWorkoutZoneGroup 有何区别?
HKWorkoutZoneConfiguration 为某个量值类型定义一整套完整的区间:有序的区间,以及它们所适用的量值类型。2 HKWorkoutZoneGroup 同时包含该配置与某量值类型的各区间停留时长数据,因此一个组会告诉您边界,以及用户在每个区间内停留了多久。3 对于已结束的训练,您从已完成训练实例中读取区间组;对于实时区间信息,则从实时来源读取。3
当用户从未设置过区间时,preferredWorkoutZoneConfiguration(for:) 返回什么?
若用户未设置自定义值,它返回系统生成的区间;只有当用户根本没有为该量值类型配置过区间时,才返回 nil。4 当用户在「健康」设置中手动设置过区间时,该方法返回那些区间,并且它们保持不变,直到用户编辑为止;系统生成的区间则随用户健康指标的变化定期更新。4 对 nil 做分支判断,以提供您自己的回退默认值。
为什么保存 menopausalState 样本时会报日期错误?
因为 menopausalState 记录的是单一时间点上的状态,框架要求样本的起始日期与结束日期完全相同,日期不一致的样本在保存时会产生错误。5 把两者设为同一个 Date。这条约束也正是它与 bleedingAfterMenopause 的区别所在——后者表示一段区间,要求起始与结束不同。7
bleedingAfterMenopause 与月经流量有何不同?
绝经之后月经已经停止,因此绝经后出血在临床上既不同于月经流量,也不同于经间期出血;Apple 正是出于这个原因为它单独设了一个分类类型。7 每个 bleedingAfterMenopause 样本存储一段区间和一个强度值;至于相关的状态追踪,则使用 menopausalState 类型。7
完整的 Apple 生态系统系列文章与本文并列:本文所基于的 iOS 26 HealthKit 授权与样本类型模式;区间数据在实时会话中产生之处的 watchOS 训练生命周期;管辖腕上后台执行的 watchOS 执行环境契约;以及把 HealthKit 定位为数据源层的 iOS 应用的三个面。系列主页是 Apple 生态系统系列。想了解更宽泛的「iOS 结合 AI 代理」语境,请参阅 iOS 代理开发指南。
参考资料
-
Apple Developer Documentation: HealthKit. 涵盖量值类型、分类类型、样本,以及 iOS 27 训练区间与生殖健康新增内容的框架参考文档。 ↩
-
Apple Developer Documentation:
HKWorkoutZoneConfiguration(iOS 27.0)。”A structure that defines a complete set of zones for a quantity type.” 声明为struct HKWorkoutZoneConfiguration;包含一个有序的区间数组,标明其量值类型,并支持系统生成、用户配置或应用提供的自定义区间。 ↩↩↩↩↩↩ -
Apple Developer Documentation:
HKWorkoutZoneGroup(iOS 27.0)。”A structure that contains zone configuration and time-in-zone data for a quantity type.” 声明为struct HKWorkoutZoneGroup;将一个配置与各区间停留时长数据结合在一起,可从已完成训练实例及实时训练来源访问。 ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
preferredWorkoutZoneConfiguration(for:)(iOS 27.0)。”Returns someone’s preferred zone configuration for the specified quantity type.” 声明为func preferredWorkoutZoneConfiguration(for quantityType: HKQuantityType) async throws -> HKWorkoutZoneConfiguration?;返回手动配置的区间、系统生成的区间,或nil。 ↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
menopausalState(iOS 27.0)。”An identifier for samples that record a person’s menopausal state.” 声明为static let menopausalState: HKCategoryTypeIdentifier;时间点样本要求起始与结束日期相同,否则保存失败。 ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
HKCategoryValueMenopausalState(iOS 27.0)。”A value that indicates the menopausal state at a recorded point in time.” 声明为enum HKCategoryValueMenopausalState;应用可查询一段时间内的多个样本,以推导出活跃期、过渡或当前状态。 ↩↩↩↩ -
Apple Developer Documentation:
bleedingAfterMenopause(iOS 27.0)。”An identifier for samples that record bleeding after menopause.” 声明为static let bleedingAfterMenopause: HKCategoryTypeIdentifier;在临床上与月经流量明确区分,每个样本是一段带强度值的区间。 ↩↩↩↩↩↩↩↩↩ -
Apple, WWDC26 session 207, “Deliver workout insights with HealthKit workout zones.” developer.apple.com/videos/play/wwdc2026/207. iOS 27 中心率与骑行功率区间集成进 HealthKit 后,每个区间内的时长会在训练过程中根据传入样本自动计算,应用从
HKWorkout或HKWorkoutActivity上的zoneGroupsByType字典读回。 ↩