WWDC Quick Look 💓 By SwiftGGTeam
HealthKitのワークアウトゾーンで運動インサイトを届ける

HealthKitのワークアウトゾーンで運動インサイトを届ける

元の動画を見る

ハイライト

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)
    )
}

重要ポイント:

  • zoneGroupsByTypeHKQuantityTypeをキーにする辞書です。HKQuantityType(.heartRate)を渡すと心拍ゾーンを取得でき、サイクリングパワーのquantity typeを渡すとパワーゾーンを取得できます
  • 返されるHKWorkoutZoneGroupには、configuration(ゾーン構成)とzoneDurations(各ゾーンの累積時間)という2つの中核属性があります
  • zoneDurationsはゾーンしきい値の小さい順に並んでいるため、そのまま棒グラフの描画に使えます

HKWorkoutZoneConfigurationの構造も詳しく見る価値があります。

  • quantityType: この構成に対応するデータ型。心拍数またはサイクリングパワー
  • source: ゾーンしきい値の由来。システムの自動計算、設定内でのユーザー手動構成、またはアプリが提供したカスタム構成
  • zones: ゾーン配列。各ゾーンにはindexminimummaximumの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は高強度を表し、長期トレンドからトレーニングが妥当か見えてくる
  • 始め方: 過去のHKWorkoutzoneGroupsByTypeを読み取り、各ゾーンの時間を累積し、Swift Chartsでトレンドグラフを描く

4. サイクリングパワーゾーンをサポートする

  • 何をするか: サイクリングアプリにパワーゾーン対応を追加する。構造は心拍ゾーンと完全に同じ
  • なぜやる価値があるか: サイクリングユーザーはパワートレーニングを広く使いますが、FTPに基づくパワーゾーン計算は常にハードルでした。HealthKitが引き受ければ、開発者は直接読み取るだけでよい
  • 始め方: quantity typeをHKQuantityType(.heartRate)からサイクリングパワーに対応するtypeへ切り替える。残りのコード構造は変わらない

5. カスタムトレーニングプラットフォームのゾーンモデル

  • 何をするか: トレーニングプラットフォーム独自のゾーンアルゴリズム(HRVや乳酸閾値に基づくモデルなど)がある場合、カスタムゾーン構成でシステム標準値を上書きする
  • なぜやる価値があるか: HealthKitのゾーン基盤(リアルタイム通知、ワークアウト間保存)を維持しながら、プラットフォーム独自の差別化アルゴリズムを示せる
  • 始め方: ゾーン境界しきい値を計算し、HKWorkoutZoneConfigurationを作成し、beginCollection前にsetCustomZoneConfigurationで設定する

関連セッション

コメント

GitHub Issues · utterances