WWDC Quick Look 💓 By SwiftGGTeam
Build a multi-device workout app

Build a multi-device workout app

元の動画を見る

ハイライト

iOS 17、watchOS 10、iPadOS 17 では HealthKit に 3 つの中核機能が追加されました。Apple Watch と iPhone 間のリアルタイム運動ミラーリング、サイクリング専用データ型(ケイデンス、パワー、速度、FTP)、および iPad 向け HealthKit です。1 セットの API で 3 デバイスをカバーする運動 App を構築できます。


主要内容

課題: 運動 App はかつて Watch 上だけで完結していた

Apple Watch 向け運動 App を開発したことがあるなら、こんな場面を知っているはずです。ユーザーが屋外でサイクリングし、Watch を手首に付けたままデータを見ようと腕を上げる — 走行中には安全でも快適でもありません。ハンドルに固定した iPhone や、室内トレーナー前の iPad など、より大きな画面は HealthKit のリアルタイムデータが Watch でしか取れないため、使われずに放置されます。

さらに、サイクリングで生まれるデータは心拍やカロリーよりはるかに多様です。ケイデンス(1 分あたりのペダル回転数)、パワー(ワット出力)、速度 — watchOS 9 以前、HealthKit はこれらを直接サポートしていませんでした。開発者は指標を諦めるか、Bluetooth センサーを自前で統合し、データを手動解析して HealthKit に戻す方法を自分で考える必要がありました。

Bluetooth センサーの互換性も落とし穴です。ブランドごとにデータ形式とペアリング手順が異なります。ユーザーは別途購入・ペアリング・キャリブレーションが必要です。開発者にとっては、フィットネスと無関係な Bluetooth 通信コードの保守を意味します。

Apple のアプローチ: 3 デバイス連携 + サイクリングのネイティブサポート

WWDC 2023 で Apple はこれらの問題に 3 つの重要機能で応えました。

  1. ミラーリング運動セッション: Watch 上のリアルタイム運動データを iPhone にミラーし、状態を自動同期。iPhone 側でより豊富なデータパネルを表示できます。
  2. サイクリング専用データ型: HealthKit がケイデンス、パワー、速度、FTP(Functional Threshold Power)を直接サポート。Watch が Bluetooth センサーから自動収集して HealthKit に書き込みます。
  3. iPad 向け HealthKit: HealthKit と Health App が iPad に登場。iCloud で健康データを同期し、iPad を大画面の運動データパネルにできます。

3 つの機能は連携します。Watch がデータを収集し、iPhone がリアルタイムミラー表示、iPad が大画面での振り返りと分析を担います。1 回のライドで生まれたデータは、すべてのデバイスで利用可能です。


詳細

ミラーリング運動セッション: Watch で収集、iPhone で表示

今回のアップデートで最も重要な新 API です。Watch 上で HKWorkoutSession がプライマリセッションとして動作し、startMirroringToCompanionDevice() を呼ぶと、ペアリングされた iPhone が自動的にコンパニオン App を起動し、ミラーセッションを受け取ります(04:15)。

ミラーセッションはプライマリセッションのすべての状態変更 — 開始、一時停止、再開、終了 — を同期します。両端の状態を手動管理する必要はありません。

Watch 側でミラーリングを開始:

// Apple Watch — ワークアウトを開始してミラーリングを有効化
let configuration = HKWorkoutConfiguration()
configuration.activityType = .cycling
configuration.locationType = .outdoor

let session = HKWorkoutSession(
    healthStore: healthStore,
    configuration: configuration
)

let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(
    healthStore: healthStore,
    workoutConfiguration: configuration
)

// ワークアウトを開始
session.startActivity(with: Date())
builder.beginCollection(withStart: Date()) { success, error in
    // データ収集が開始済み
}

// iPhone へのミラーリングを有効化
try await healthStore.startMirroringToCompanionDevice(session)

キーポイント:

  • HKWorkoutConfiguration で運動タイプと場所(屋外/屋内)を定義し、GPS や高度の収集方法に影響します。
  • HKLiveWorkoutDataSource は Watch 側のリアルタイムデータ入口で、Watch センサーとペアリング済み Bluetooth デバイスから自動収集します。
  • startMirroringToCompanionDevice() は Watch 側でのみ呼べます。コンパニオン iPhone App のインストールと同一 iCloud アカウントが必要です。
  • Watch App はフォアグラウンドにいる間、ミラーリングが継続します。

iPhone 側では、アンカークエリでリアルタイムデータを受信:

// iPhone — ミラーリングデータを受信
class MirroringManager: ObservableObject {
    let healthStore = HKHealthStore()
    @Published var heartRate: Double = 0

    func observeMirroredData() {
        let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!

        let query = HKAnchoredObjectQuery(
            type: heartRateType,
            predicate: nil,
            anchor: nil,
            limit: HKObjectQueryNoLimit
        ) { _, samples, _, _, _ in
            guard let quantitySamples = samples as? [HKQuantitySample] else { return }
            for sample in quantitySamples {
                let bpm = sample.quantity.doubleValue(for: HKUnit(from: "count/min"))
                DispatchQueue.main.async {
                    self.heartRate = bpm
                }
            }
        }

        healthStore.execute(query)
    }
}

キーポイント:

  • HKAnchoredObjectQuery は新データ到着時にコールバック — ポーリング不要です。
  • ミラーセッション開始後、iPhone App はバックグラウンドで起動され、セッション設定に約 10 秒使えます。
  • HealthKit クエリ結果はすべてバックグラウンドスレッドで返るため、UI 更新前にメインスレッドへ切り替えてください。

Watch と iPhone は sendData でカスタムデータも相互送信できます(06:30):

// Watch から iPhone へカスタムイベントを送信
let lapData = "lapCompleted".data(using: .utf8)!
try await healthStore.sendData(lapData, toRemoteWorkoutSession: session)

// iPhone 側で受信 — HKWorkoutSessionDelegate 経由
func workoutSession(_ workoutSession: HKWorkoutSession,
                    didReceiveDataFromRemoteWorkoutSession data: [Data]) {
    for item in data {
        if let message = String(data: item, encoding: .utf8) {
            print("Watch からのメッセージを受信: \(message)")
        }
    }
}

キーポイント:

  • sendData は両端間で任意のバイナリデータを送れます — ラップマーカー、インターバル切り替え、ユーザー通知などのカスタムイベント向け。
  • 受信側は HKWorkoutSessionDelegatedidReceiveDataFromRemoteWorkoutSession でデータを取得します。
  • 高頻度データチャンネルではありません。連続センサーストリームではなく、少量のイベント級データ向けです。

サイクリングデータ型: センサー統合を自前で書く必要がない

watchOS 10 では HealthKit に 4 つのサイクリング専用データ型が追加されました(08:20):

データ型HealthKit Identifier単位
ケイデンスcyclingCadence回/分 (count/min)
パワーcyclingPowerワット (W)
速度cyclingSpeedm/秒 (m/s)
FTPcyclingFunctionalThresholdPowerワット (W)

Apple Watch は Bluetooth サイクリングセンサー(パワーメーター、ケイデンスセンサー)に直接接続し、データを自動収集して HealthKit に書き込めます。開発者は Bluetooth コードを 1 行も書く必要がありません。

データソース設定時に、必要な型を有効化します:

let dataSource = HKLiveWorkoutDataSource(
    healthStore: healthStore,
    workoutConfiguration: configuration
)

// ケイデンス収集を有効化
dataSource.enableCollection(
    for: HKQuantityType(.cyclingCadence),
    predicate: nil
)

// パワー収集を有効化
dataSource.enableCollection(
    for: HKQuantityType(.cyclingPower),
    predicate: nil
)

// 速度収集を有効化
dataSource.enableCollection(
    for: HKQuantityType(.cyclingSpeed),
    predicate: nil
)

キーポイント:

  • enableCollectionHKLiveWorkoutDataSource に指定型の収集を開始させます。呼ばないと、センサーが接続されていても記録されません。
  • Watch が Bluetooth センサーのペアリングと読み取りを処理します。ペアリングはシステム設定で行い、App の介入は不要です。
  • 認可リクエスト時、これらの型を typesToRead に追加してください。

HKWorkoutBuilderaddSamplesHandler でサイクリングデータをリアルタイム処理(10:45):

builder.addSamplesHandler = { [weak self] samples in
    for sample in samples {
        switch sample.sampleType {
        case HKQuantityType(.cyclingCadence):
            if let quantity = (sample as? HKQuantitySample)?.quantity {
                let rpm = quantity.doubleValue(for: HKUnit(from: "count/min"))
                self?.cadence = Int(rpm)
            }
        case HKQuantityType(.cyclingPower):
            if let quantity = (sample as? HKQuantitySample)?.quantity {
                let watts = quantity.doubleValue(for: .watt())
                self?.power = Int(watts)
            }
        case HKQuantityType(.cyclingSpeed):
            if let quantity = (sample as? HKQuantitySample)?.quantity {
                let mps = quantity.doubleValue(for: HKUnit.meter().unitDivided(by: .second()))
                self?.speed = mps
            }
        default:
            break
        }
    }
}

キーポイント:

  • addSamplesHandler は新サンプルごとにコールバック — リアルタイム UI 更新に適しています。
  • HKQuantitySample には値とタイムスタンプがあり、doubleValue(for:) で必要な単位に変換できます。
  • FTP は HealthKit が複数回のライドデータから自動推定 — ユーザーが 20 分全力テストを手動で行う必要はありません。
  • [weak self] で循環参照を避けます — builder が handler を保持するため、handler は builder を保持するオブジェクトを強参照すべきではありません。

iPad 向け HealthKit: 大画面の健康データパネル

iPadOS 17 で初めて HealthKit サポートと Health App が導入されました。iPad に心拍センサーや GPS はありませんが、iCloud 同期で他デバイスの健康データを取得できます(14:10)。

iPad で HealthKit 認可をリクエストする方法は iPhone と異なり、HealthKitUI フレームワークが必要です:

import HealthKitUI
import SwiftUI

struct WorkoutView: View {
    let healthStore = HKHealthStore()
    let typesToShare: Set<HKSampleType> = [.workoutType()]
    let typesToRead: Set<HKObjectType> = [
        HKObjectType.quantityType(forIdentifier: .cyclingCadence)!,
        HKObjectType.quantityType(forIdentifier: .cyclingPower)!,
        HKObjectType.quantityType(forIdentifier: .cyclingSpeed)!,
    ]

    var body: some View {
        VStack {
            // ワークアウトデータ表示 UI
        }
        .healthDataAccessRequest(
            store: healthStore,
            shareTypes: typesToShare,
            readTypes: typesToRead,
            trigger: true,
            completion: { result in
                switch result {
                case .success:
                    print("認可に成功")
                case .failure(let error):
                    print("認可に失敗: \(error)")
                }
            }
        )
    }
}

キーポイント:

  • iPad の HealthKit データは iCloud 同期由来で、リアルタイム収集ではありません — 運動後の振り返りと分析向けです。
  • 認可 UI は HealthKitUIhealthDataAccessRequest(SwiftUI)または HKHealthStoreauthorizationViewControllerPresenter(UIKit)を使う必要があります。
  • iPad ではマルチウィンドウに対応 — 異なるウィンドウから認可がトリガーされる可能性があります。
  • iPadOS 17 より前の iPad では HKHealthStore.isHealthDataAvailable()false を返すため、バージョンチェックが必要です。

iPad で過去のサイクリングデータをクエリ:

let predicate = HKQuery.predicateForWorkouts(with: .cycling)
let sortDescriptor = NSSortDescriptor(
    key: HKSampleSortIdentifierStartDate,
    ascending: false
)

let query = HKSampleQuery(
    sampleType: .workoutType(),
    predicate: predicate,
    limit: 1,
    sortDescriptors: [sortDescriptor]
) { _, samples, error in
    guard let workout = samples?.first as? HKWorkout else { return }

    let distance = workout.totalDistance?.doubleValue(
        for: HKUnit.meterUnit(with: .kilo)
    )
    let energy = workout.totalEnergyBurned?.doubleValue(
        for: .kilocalorie()
    )

    DispatchQueue.main.async {
        self.distance = distance
        self.calories = energy
    }
}

healthStore.execute(query)

キーポイント:

  • predicateForWorkouts(with:) で運動タイプをフィルタし、そのタイプの workout をすべて返します。
  • HKWorkout には totalDistancetotalEnergyBurned があり、追加クエリは不要です。
  • ケイデンス、パワーなど詳細指標は、workout の UUID でフィルタした HKStatisticsQuery を使います。

重要ポイント

1. ハンドルバー用 iPhone ダッシュボードを作る

やること: iPhone App をライド中のリアルタイムダッシュボードに。ミラーセッションで Watch からケイデンス、パワー、速度、心拍を取得し、ハンドルに固定した iPhone の大画面・大文字で表示。

価値: startMirroringToCompanionDevice() により iPhone が Watch センサーデータにほぼ遅延なくアクセスできます。HKLiveWorkoutDataSourceenableCollection と組み合わせれば、サイクリング指標はすべて自動で揃い、センサー統合コードは不要です。

始め方: Watch 側で .cycling 型の HKWorkoutSession を作成し、startMirroringToCompanionDevice() を呼ぶ。iPhone 側は HKAnchoredObjectQuery でリアルタイムデータを監視し、SwiftUI の大きな TextGauge で各指標を表示。

2. 室内サイクリング用 iPad トレーニングパネルを作る

やること: iPad App で最近のサイクリングデータトレンド — FTP 変化曲線、パワーゾーン分布、ケイデンス安定性 — を表示し、Apple Watch がリアルタイム収集したデータに基づくトレーニング指導を提供。

価値: iPad の大画面は複雑なデータ表示に向いています。iPadOS 17 以降の HealthKit では、Watch が収集したサイクリングデータが iCloud 経由で iPad に自動同期されます。FTP はシステムが自動推定 — ユーザーが手入力する必要はありません。

始め方: iPad で HKSampleQuery により .cycling 型の過去 workout をクエリし、cyclingPowercyclingCadencecyclingFunctionalThresholdPower を抽出。Swift Charts でパワー曲線と FTP トレンドを描画。

3. サイクリングソーシャル App のデータ収集層として HealthKit を使う

やること: HealthKit のサイクリングデータをソーシャル App のデータソースに。各ライドのルート、パワー、速度を自動記録し、手入力やサードパーティセンサー App に依存しない。

価値: 以前のサイクリング App はセンサー統合を自前で行うか、Strava など第三方データに依存していました。HealthKit がケイデンス、パワー、速度を直接提供するため、ソーシャル機能に集中でき、収集層はシステムに任せられます。

始め方: .cyclingCadence.cyclingPower.cyclingSpeed の読み取り権限をリクエスト。HKStatisticsCollectionQuery で日/週単位に集計。HKWorkoutRouteBuilder で GPS ルートを関連付け。プライバシーに注意 — ユーザーが明示的に認可した後だけデータを読み取る。

4. FTP 自動追跡ツールを作る

やること: HealthKit が自動推定する FTP 値の長期変化を追跡し、FTP が向上したときに通知とモチベーションを提供。

価値: FTP はサイクリング能力の最も重要な指標ですが、従来は 20 分全力ライドテストが必要 — 苦痛で不正確です。HealthKit は日常ライドから FTP を推定するため、より現実的で継続的なデータが得られます。

始め方: HKAnchoredObjectQuerycyclingFunctionalThresholdPower の変化を監視し、各更新の時刻と値を記録。Swift Charts で長期トレンドを描画。FTP が新記録を更新したときにローカル通知を送る。


関連セッション

  • Meet watchOS 10 — watchOS 10 の全体アップデート。新デザイン言語と Smart Stack。本 Session の Watch 側 API を理解する前提
  • Update your app for watchOS 10 — 既存 watchOS App を watchOS 10 に移行する実践ガイド。SwiftUI 新コンポーネントとレイアウトパターンをカバー
  • Platforms State of the Union — WWDC 2023 プラットフォーム技術総覧。HealthKit 更新が各プラットフォームエコシステムのどこに位置するか

コメント

GitHub Issues · utterances