iOS 27 的 HealthKit:訓練心率區間與全新類型
多年來,訓練心率區間在 HealthKit 裡一直是個沒有正式身分的存在:每一款會畫出五段區間長條圖的跑步或騎乘 App,都得自己算出這些區間——撈出原始的 HKQuantitySample 心率讀數,依照寫死或由使用者輸入的門檻一段段分桶,再手動把落在每段裡的時間加總起來。iOS 27 終結了這種手工活。區間如今成為由系統產生的結構化 HealthKit 類型,使用者可以在「健康」設定中自行編輯,而您的 App 讀回來時,各區間的累計時間早已算好。同一版更新還新增了兩個生殖健康類別類型——menopausalState 與 bleedingAfterMenopause——補上了 HealthKit 週期追蹤模型中的一塊缺口。1
iOS 27 的新增功能有兩條主線,而它們形狀相同。訓練區間這條線,把過去由 App 自行掌管的計算搬進框架,讓同一個人的區間定義在每一款查詢它的 App 之間保持一致。新類型這條線,則為 HealthKit 過去無法表達的人生階段,提供了一個帶型別的類別樣本。兩者都遵循既有的 HealthKit 文法:量值用 quantity type,事件與狀態用 category type。本文會對照 Apple 官方文件逐一說明,並沿用本系列一貫的切入角度:一款已經整合 HealthKit 的 App,要再做哪些事才能取得每一項能力。
重點摘要
HKWorkoutZoneConfiguration為某個 quantity type 定義一整組完整的區間。系統會依使用者的健康指標自動產生區間,使用者可以在「健康」設定中編輯,App 也能為特定訓練提供自訂區間。2HKWorkoutZoneGroup把區間設定與其落在各區間的時間資料配成一對。您從已完成的訓練實例讀回它,就能拿到框架已算好的各區間時長,無須自己對心率樣本分桶。3HKHealthStore上的preferredWorkoutZoneConfiguration(for:)會回傳使用者對某 quantity type 偏好的區間設定,讓您的圖表與他在各處看到的門檻一致。4menopausalState加上HKCategoryValueMenopausalState列舉,可記錄某個時間點的更年期狀態;該樣本的開始日期與結束日期必須完全相同,否則儲存會失敗。56bleedingAfterMenopause把停經後出血記錄為一段帶有強度值的區間,並在臨床上與經血明確區分。7
訓練區間,過去一直得自己算
在 iOS 27 之前,想做心率區間的訓練 App 得包辦整個流程。它會查詢訓練時間範圍內的心率 HKQuantitySample 資料,決定區間邊界落在哪裡(220 減年齡、乳酸閾值百分比,或使用者在設定畫面裡輸入的數值),依序走過所有樣本,並累計落在每段的秒數。每款 App 都用自己的一套邊界算法,所以一個人若用兩款 App 跑步,看到的會是兩種不同的「Zone 3」。沒有任何區間資料存在 HealthKit 裡,因此這些資料既不會同步,也無法跨 App 共用。
iOS 27 引入了 HKWorkoutZoneConfiguration,這是一個為某 quantity type 定義一整組完整區間的結構。2 此設定帶有一個依序排列的區間陣列,並標明它所套用的 quantity type。真正關鍵的改變在於區間從何而來。Apple 官方文件指出,系統會依使用者的健康指標自動產生區間;使用者也可以在「健康」設定中手動設定區間;或由 App 為特定訓練提供自訂區間。2 區間定義不再是各 App 私有的細節,而成為框架掌管的共用狀態。
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,它包含某 quantity type 的區間設定與落在各區間的時間資料。3 一個 zone group 把 HKWorkoutZoneConfiguration 與各區間的時間資料結合起來,因此讀取一個 group,您同時拿到了邊界,以及使用者在每一段裡待了多久。Apple 官方文件說明,可從已完成的訓練實例存取 zone group,以取得已結束訓練的區間資料;也可從進行中的訓練來源存取,取得當前活動階段的即時區間資訊。3 分桶由框架完成,您的程式碼只負責讀結果。
在第 207 場議程中,Apple 表示,當訓練區間整合進 HealthKit 後,每段區間的時間會在訓練過程中依進來的樣本自動算出,因此 App 直接從 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)
}
}
第三塊把設定綁到「人」而非「App」。preferredWorkoutZoneConfiguration(for:) 是 HKHealthStore 上的一個實例方法,會回傳某人對指定 quantity type 偏好的區間設定:4
func preferredWorkoutZoneConfiguration(
for quantityType: HKQuantityType
) async throws -> HKWorkoutZoneConfiguration?
Apple 官方文件清楚交代了回傳的約定:此方法會回傳使用者在「健康」設定中手動設定的區間;若使用者尚未設定自訂值,則回傳系統產生的區間;若使用者根本未對該 quantity type 設定過任何區間,則回傳 nil。4 系統產生的區間會隨使用者健康指標變化而週期性更新;由使用者手動設定的區間則維持不變,直到他再次編輯。文件所述的用意是:讓各 App 顯示的區間資訊,在每一次訓練都與使用者的偏好一致。4 當您希望自己的 Zone 3 與使用者在各處看到的 Zone 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,則用於某次訓練需要不同於使用者預設區間的情況,例如一場有自己分段的結構化間歇訓練。多數 App 想要的是偏好設定;自訂這條路,是留給訓練本身的結構非要有自己一套區間時用的。
全新類別類型:menopausalState 與 bleedingAfterMenopause
HealthKit 的週期追蹤模型能處理月經,卻沒有帶型別的方式來記錄一個人處於更年期過渡的哪個階段,也無法記錄月經結束之後發生的出血。iOS 27 把這兩者都新增為類別類型,沿用 iOS 26 HealthKit 一文 介紹正念紀錄與睡眠時所用的同一套 HKCategorySample 模式。
menopausalState 是用於記錄使用者更年期狀態樣本的類別類型識別碼。5 每個樣本都是某個時間點的單筆紀錄,而 Apple 官方文件施加了一條硬性限制:開始日期必須等於結束日期,若儲存兩者不同的樣本會產生錯誤。5 每個樣本的值是 HKCategoryValueMenopausalState 列舉的其中一個 case,表示某個被記錄時間點上的更年期狀態。6 由於 Apple 的說明中略去了完整的 case 清單,請把這個列舉本身當作有效值的權威來源,而非自行臆測 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 官方文件指出,App 可讀取一段時間內的多個樣本,從中推導出更高層次的詮釋:活躍期、轉變,或當前狀態。6 單一樣本所主張的,是某個特定狀態在某個特定日期成立,而把這些樣本串接成時間軸的工作,框架交由 App 完成。Apple 把每筆紀錄詮釋為一次狀態變化、一次「該狀態於此日期成立」的確認,或兩者兼具——端看 App 如何讀取這一系列樣本。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 的訓練或健康 App,可以逐步加入 iOS 27 的這些介面;這些都不會動到 iOS 26 模式一文 所涵蓋的授權或樣本儲存底層架構。
- 刪掉您的區間算式。 任何在手動計算心率區間的 App,都該針對相關 quantity type 呼叫
preferredWorkoutZoneConfiguration(for:),並繪製回傳的設定,只在呼叫回傳nil時才退回自己的預設值。4 您的區間如今與使用者在系統各處看到的一致。 - 改用
HKWorkoutZoneGroup,別再對樣本分桶。 製作已完成訓練的摘要時,取出 zone group 並繪製其預先算好的各區間時間資料,而不是自己走過原始心率樣本、累計時長。3 - 只在某次訓練需要自己一套分段時,才提供自訂的
HKWorkoutZoneConfiguration。 帶有專屬區間的結構化間歇訓練,才是動用自訂設定的場合;其餘一律使用使用者偏好的設定。2 - 把新類別類型加進授權請求。 週期追蹤 App 要把
menopausalState與bleedingAfterMenopause加進其分享與讀取的集合,接著把menopausalState記錄為時間點樣本(開始等於結束),把bleedingAfterMenopause記錄為帶強度的區間樣本。57 - 依類型逐一檢查您的開始/結束日期。 更年期狀態若開始與結束不同就會儲存失敗;出血樣本是區間,預期兩者不同。在上線儲存路徑之前,先把時間窗的形狀弄對。57
常見問題
在 iOS 27 裡,我還得自己算心率區間嗎?
不必,至少在常見情況下不必。preferredWorkoutZoneConfiguration(for:) 會回傳使用者對某 quantity type 偏好的區間設定(他在「健康」設定中手動設定的區間、系統產生的區間,或在他從未設定任何區間時回傳 nil),而 HKWorkoutZoneGroup 則承載框架已為某次訓練算好的各區間時間資料。34 只有當某次訓練需要不同於使用者預設的自訂分段時,您才自己計算區間——此時為該次訓練建立一個 HKWorkoutZoneConfiguration。2
HKWorkoutZoneConfiguration 與 HKWorkoutZoneGroup 有什麼差別?
HKWorkoutZoneConfiguration 為某 quantity type 定義一整組完整的區間:依序排列的區間,以及它們所套用的 quantity type。2 HKWorkoutZoneGroup 同時包含這份設定與某 quantity type 落在各區間的時間資料,因此一個 group 會告訴您邊界,以及使用者在每段區間裡待了多久。3 已結束的訓練從已完成的訓練實例讀取 zone group,即時區間資訊則從進行中的來源讀取。3
當使用者從未設定過區間時,preferredWorkoutZoneConfiguration(for:) 會回傳什麼?
若使用者尚未設定自訂值,它會回傳系統產生的區間;只有在使用者根本未對該 quantity type 設定過任何區間時,才回傳 nil。4 當使用者在「健康」設定中手動設定過區間,此方法會回傳那些區間,且它們維持不變,直到使用者編輯;系統產生的區間則會隨使用者健康指標變化而週期性更新。4 請依 nil 進行分支,以提供您自己的退回預設值。
為什麼儲存 menopausalState 樣本會出現日期錯誤?
因為 menopausalState 記錄的是單一時間點的狀態,框架要求樣本的開始日期與結束日期完全相同,日期不同的樣本在儲存時會產生錯誤。5 請把兩者設為同一個 Date。這條限制正是它與 bleedingAfterMenopause 的區別所在——後者代表一段區間,預期開始與結束不同。7
bleedingAfterMenopause 與經血有何不同?
停經之後月經已結束,因此停經後出血在臨床上與經血、與經間出血皆有別;Apple 正因如此給了它一個獨立的類別類型。7 每個 bleedingAfterMenopause 樣本儲存一段區間與一個強度值;至於相關的狀態追蹤,則使用 menopausalState 類型。7
完整的 Apple 生態系列就在本文一旁:本文所立足的 iOS 26 HealthKit 授權與樣本類型模式;區間資料在活動階段中產生之處的 watchOS 訓練生命週期;規範手腕端背景執行的 watchOS 執行環境契約;以及把 HealthKit 定位為資料來源層的 iOS App 的三個介面。系列總覽在 Apple 生態系列。若想了解 iOS 結合 AI 代理的更廣脈絡,請參閱 iOS 代理開發指南。
參考資料
-
Apple Developer Documentation: HealthKit. The framework reference covering quantity types, category types, samples, and the iOS 27 workout-zone and reproductive-health additions. ↩
-
Apple Developer Documentation:
HKWorkoutZoneConfiguration(iOS 27.0). “A structure that defines a complete set of zones for a quantity type.” Declared asstruct HKWorkoutZoneConfiguration; contains an ordered array of zones, identifies its quantity type, and supports system-generated, person-configured, or app-supplied custom zones. ↩↩↩↩↩↩ -
Apple Developer Documentation:
HKWorkoutZoneGroup(iOS 27.0). “A structure that contains zone configuration and time-in-zone data for a quantity type.” Declared asstruct HKWorkoutZoneGroup; combines a configuration with time-in-zone data, accessible from completed-workout instances and from live-workout sources. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
preferredWorkoutZoneConfiguration(for:)(iOS 27.0). “Returns someone’s preferred zone configuration for the specified quantity type.” Declared asfunc preferredWorkoutZoneConfiguration(for quantityType: HKQuantityType) async throws -> HKWorkoutZoneConfiguration?; returns manually configured zones, system-generated zones, ornil. ↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
menopausalState(iOS 27.0). “An identifier for samples that record a person’s menopausal state.” Declared asstatic let menopausalState: HKCategoryTypeIdentifier; point-in-time samples require identical start and end dates or the save fails. ↩↩↩↩↩↩↩ -
Apple Developer Documentation:
HKCategoryValueMenopausalState(iOS 27.0). “A value that indicates the menopausal state at a recorded point in time.” Declared asenum HKCategoryValueMenopausalState; apps can query multiple samples over time to derive active periods, transitions, or current state. ↩↩↩↩ -
Apple Developer Documentation:
bleedingAfterMenopause(iOS 27.0). “An identifier for samples that record bleeding after menopause.” Declared asstatic let bleedingAfterMenopause: HKCategoryTypeIdentifier; clinically distinct from menstrual flow, each sample is an interval with an intensity value. ↩↩↩↩↩↩↩↩↩ -
Apple, WWDC26 session 207, “Deliver workout insights with HealthKit workout zones.” developer.apple.com/videos/play/wwdc2026/207. With heart rate and cycling power zones integrated into HealthKit in iOS 27, the time in each zone is calculated automatically based on incoming samples during the workout, and an app reads it back from the
zoneGroupsByTypedictionary onHKWorkoutorHKWorkoutActivity. ↩