ハイライト
iOS 27とwatchOS 27では、心拍ゾーン(Heart Rate Zones)とサイクリングパワーゾーン(Cycling Power Zones)がHealthKitに直接統合されます。開発者はゾーンしきい値を自分で計算したり、ゾーンロジックを保守したりする必要がありません。新しいAPIを呼び出すだけで、システムが計算したゾーンデータを読み取り、リアルタイムのゾーン変化通知を受け取り、またはカスタムゾーン構成を提供できます。
主要内容
以前のやり方: すべてのフィットネスアプリが自分で計算していた
フィットネスアプリを作るなら、心拍ゾーンは避けて通れません。ユーザーはランニング後に、どの強度ゾーンにどれだけ滞在したかを見たいものです。しかし開発者の選択肢は限られていました。
- ユーザーの年齢と安静時心拍数から5つのゾーンしきい値を計算する式を自前で保守する
- 各心拍サンプルがどのゾーンに入るかを自前で比較し、時間を累積する
- iPhoneとApple Watchでゾーンデータが一致するように、複数デバイス同期を自前で処理する
- アプリごとにアルゴリズムが違うため、ユーザーがアプリを変えるとゾーン定義が変わってしまう
さらに厄介なのは、サイクリングパワーゾーン(Cycling Power Zones)が機能的しきい値パワー(FTP)を基盤にしており、計算ロジックが心拍ゾーンとまったく異なることです。1つのアプリで両方をサポートしようとすると、コードの複雑さは一気に倍増します。
Appleが追加したもの
iOS 27とwatchOS 27のHealthKitにはWorkout Zone体系が追加され、心拍ゾーンとサイクリングパワーゾーンの計算、保存、同期をすべて引き受けます。
システムはユーザーの年齢と安静時心拍数に基づいて、心拍ゾーンのしきい値を自動計算します。ユーザーは「ヘルスケア」アプリでこれらのしきい値を手動調整することもできます。すべてのゾーン構成はHealthKitを通じてデバイス間で同期され、アプリを変えても変わりません。
開発者は必要な権限をリクエストするだけで、システムがすでに計算したゾーンデータを読み取ったり、自分のカスタムゾーンを提供したりできます。
何が解決されるか
開発者にとっては、ゾーンアルゴリズムとデバイス間同期コードを保守する必要がなくなります。ユーザーにとっては、フィットネスアプリごとの心拍ゾーン定義が一致し、データがシームレスにつながります。
詳細
完了したワークアウトのゾーンデータを読み取る
(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をキーにする辞書です。HKQuantityType(.heartRate)を渡すと心拍ゾーンを取得でき、サイクリングパワーのquantity typeを渡すとパワーゾーンを取得できます- 返される
HKWorkoutZoneGroupには、configuration(ゾーン構成)とzoneDurations(各ゾーンの累積時間)という2つの中核属性があります zoneDurationsはゾーンしきい値の小さい順に並んでいるため、そのまま棒グラフの描画に使えます
HKWorkoutZoneConfigurationの構造も詳しく見る価値があります。
quantityType: この構成に対応するデータ型。心拍数またはサイクリングパワーsource: ゾーンしきい値の由来。システムの自動計算、設定内でのユーザー手動構成、またはアプリが提供したカスタム構成zones: ゾーン配列。各ゾーンにはindex、minimum、maximumの3つの属性があります。最初のゾーンには下限がなく、最後のゾーンには上限がないため、すべての可能な値がカバーされます
リアルタイムでゾーン変化を受け取る
(07:57)
ワークアウト中、HealthKitはincomingの心拍サンプルをリアルタイムに処理し、現在どのゾーンに入っているかを判定します。ゾーンが変化したとき(たとえばZone 2からZone 3へ移ったとき)、delegateを通じてアプリへ通知します。
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更新をメインスレッドへ戻す
この仕組みにより、リアルタイムコーチ機能は非常に簡単になります。ユーザーが目標ゾーンから外れたときにアプリが通知したり、現在ゾーンを強調表示したりできます。
ユーザーの優先ゾーン構成を確認する
(09:19)
ゾーンワークアウトを開始する前に、まずユーザーが優先ゾーン構成を設定しているか確認します。
if try await builder.zoneConfiguration(for: HKQuantityType(.heartRate)) == nil {
// ユーザーが優先ゾーンを設定していないため、カスタム構成を提供する
}
重要ポイント:
HKWorkoutBuilderでもHKHealthStoreでも照会できる- 返り値が
nilの場合、ユーザーが「ヘルスケア」アプリでゾーンしきい値を構成していないことを意味する - 優先ゾーンはデバイス間で同期されるため、1台で設定すればすべてのデバイスで有効になる
カスタムゾーン構成を作成する
(09:24)
ユーザーに優先構成がない場合、またはアプリ独自のゾーンモデルがある場合は、カスタム構成を提供できます。
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を呼び出す前に設定しなければなりません - カスタム構成は現在のワークアウトのコンテキストにのみ保存されます。保存と同期はアプリ側で責任を持つ必要があります
ワークアウト間比較の注意点
(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. サイクリングパワーゾーンをサポートする
- 何をするか: サイクリングアプリにパワーゾーン対応を追加する。構造は心拍ゾーンと完全に同じ
- なぜやる価値があるか: サイクリングユーザーはパワートレーニングを広く使いますが、FTPに基づくパワーゾーン計算は常にハードルでした。HealthKitが引き受ければ、開発者は直接読み取るだけでよい
- 始め方: quantity typeを
HKQuantityType(.heartRate)からサイクリングパワーに対応するtypeへ切り替える。残りのコード構造は変わらない
5. カスタムトレーニングプラットフォームのゾーンモデル
- 何をするか: トレーニングプラットフォーム独自のゾーンアルゴリズム(HRVや乳酸閾値に基づくモデルなど)がある場合、カスタムゾーン構成でシステム標準値を上書きする
- なぜやる価値があるか: HealthKitのゾーン基盤(リアルタイム通知、ワークアウト間保存)を維持しながら、プラットフォーム独自の差別化アルゴリズムを示せる
- 始め方: ゾーン境界しきい値を計算し、
HKWorkoutZoneConfigurationを作成し、beginCollection前にsetCustomZoneConfigurationで設定する
関連セッション
- セッション303: 優れたカメラ体験を構築する — フィットネスアプリでは、トレーニング過程の記録にカメラ機能を組み合わせることがよくある
- セッション304: 完璧な写真を撮影する — 運動シーンにおける写真撮影の最適化
- セッション223: Live Activities — リアルタイムの心拍ゾーンをロック画面とDynamic Islandに表示する
- セッション262: Swift — HealthKit APIはSwift並行処理モデルを基盤に設計され、
async/awaitを多用する - セッション274: SwiftData — 過去のゾーンデータをローカルデータベースに永続化し、オフラインでワークアウト記録を確認できるようにする
コメント
GitHub Issues · utterances