View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

その他のビデオ

ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。

  • 概要
  • トランスクリプト
  • コード
  • Apple Watch用ワークアウトAppの構築

    このCode-AlongではSwiftUIとHealthKitを使用してワークアウトAppを一から構築します。タイムラインを使用してAlways On(常にオン)ステートをサポートしワークアウトの指標を更新する方法について確認します。ワークアウトAppのベストデザインプラクティスに従って構築しましょう。

    リソース

    • Build a workout app for Apple Watch
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC23

    • マルチデバイスワークアウトアプリの構築

    WWDC21

    • SwiftUIの新機能
    • watchOS 8の新機能
  • ダウンロード

    ♪ (Apple Watch用 ワークアウトAppの構築) こんにちは ブレイディです 私はフィットネスチームの エンジニアです ご参加ありがとうございます すでにたくさんの素晴らしい ワークアウトAppがあります App Storeから ダウンロードできます Apple Watchは あらゆる種類のフィットネス活動を トラッキングできる 素晴らしい製品です 挑戦的なサイクリングでも 距離と高度を記録できます 激しいワークアウト中の心拍数や 消費エネルギーを モニターすることができます 水泳では ストロークタイプを識別 したり 往復回数を数えられます これらはすべて利用して 素晴らしいワークアウト Appを作れます それでは 本日の内容をご説明いたします 今回のセッションは Code-Alongです その意味と参加方法 について説明します SwiftUIでワークアウト ビューを作成します 次にHealthKitをビューに 統合します また 常時表示状態を サポートする方法も説明します ワクワクしますね さっそく始めましょう このセッションは Code-Alongです ワークアウトAppを ゼロから一緒に作ります いくつかのコンセプトを紹介します ワークアウトAppとは何ですか? ワークアウトAppは ワークアウト中の フィットネス活動を トラッキングします ワークアウトはワンタップ で開始できます ワークアウトのセッション中は 経過時間 消費エネルギー 心拍数 距離などのライブメトリックスが 表示されます ワークアウトが終了すると サマリにワークアウト用に 記録したメトリックスが表示されます これが今日作っていくものです ワークアウトビューの 構築を始めましょう Xcodeを開いて新しい プロジェクトを開始しましょう 新しいXcodeプロジェクトの 作成をクリックします watchOS Watch App 次をクリックします ワークアウトAppに「MyWorkout」 などの名前を付けます インタフェースがSwiftUI 言語がSwiftであることを確認して 次をクリックします プロジェクトの保存場所を確定して 作成をクリックします

    インスペクタを非表示にして キャンバスのサイズを変更します

    Xcodeプレビューを使用して 再生をクリックして Appがどのように 表示されるかを確認します よかった!SwiftUI Appが 完成しました ワークアウトを開始 できるようにしましょう StartViewで ワンタップでワークアウトを 開始することができます カルーセルスタイルの レイアウトを持つリストビューでは 奥行きのある効果で垂直方向に スクロールするワークアウトの リストを提供します ワークアウトの一覧にサイクリング ランニング ウォーキングがある StartViewを作ってみましょう 「ContentView」を 「StartView」に変更しましょう ContentViewを コマンドクリックして Renameをクリックします 新しい名前として 「StartView」 と入力 MyWorkoutsApp.swiftの StartViewが NavigationViewのルートビューに なったことに注意してください

    Renameをクリック

    StartViewのリストに 表示するワークアウトタイプ の配列を定義しましょう まず HealthKitをインポートし HKWorkoutActivityType にアクセスします

    次に ワークアウトタイプ の配列を追加します

    ワークアウトリストには HKWorkoutActivityTypes のサイクリング ランニング ウォーキングが表示されます HKWorkoutActivityType 列挙型を拡張して 識別可能なプロトコルに準拠し name変数を追加して HKWorkoutActivityTypeをリストに アクセスできるようにします

    ID-computed変数は 列挙型のrawValueを返します

    name変数は大文字と小文字を 切り替えて 「ランニング」 「サイクリング」 「ウォーキング」 などの名前を返します StartViewの本体に リストビューを追加して ワークアウトのリストを 表示してみましょう

    このリストはworkoutTypes 変数をモデルとして使用します

    workoutType毎に NavigationLinkが表示します NavigationLinkは ナビゲーションベースの インターフェイスの デスティネーションを定義します ここでは デスティネーションは テキストビューになります これらのナビゲーションリンク を後で設定して 正しいワークアウトを トラッキングしていることを確認します

    パディングは ナビゲーション リンクの高さを高くして タップエリアを広くしワークアウト を簡単に開始できるようにしました

    リストはカルーセル listStyleを使用し スクロール時に奥行きのある 効果を与えます

    navigationBarTitleには 「ワークアウト」と表示されます StartViewのプレビューを 表示にはResumeをクリックします

    ライブプレビューをクリックすると スクロールできるようになります スクロールアップして カルーセル ListStyleの奥行効果確認します いいですね ワークアウトセッションは モーダルな体験として提供されます 通常 ワークアウト中に必要なのは セッション固有の機能だけです ワークアウトのリストを確認したり Appの他の部分に アクセスしたりする 必要はありません モーダルな体験の中で最も重要な アイテムを紹介することで ユーザはセッションを管理 しながら気が散るのを 最低限に抑えられるでしょう Apple Watchで ワークアウトAppを使う人は ビューがこの順番で表示される ことを期待しています 左側のコントロールビューには 進行中のセッションを制御する End Pause Resume などのボタンがあります 中央の画面では 測定値が専用画面に表示され 一目で見ることができます 画面右側の メディア再生コントロールでは ワークアウト中にメディアを コントロールできます ユーザが左右にスワイプする 場合watchOS上のTabViewは 複数の子ビューを切替できます TabViewでは ページインジケータが ビューの下部にあります TabViewはセッション中の ビューを表示するのに便利です では3つのワークアウトセッション ビューのTabViewを持つ SessionPagingViewを 作成ましょう

    ファイル>新規>ファイル...を クリックします SwiftUIビューで 次へをクリックします これに「SessionPagingView」 という名付け 作成をクリック

    TabViewで選択可能な各 ビューをモデル化するために Tab列挙型を作成しましょう

    Tab列挙型には3つのケースが あります コントロール メトリックス nowPlaying また selection という@State変数を追加して TabViewの選択項目の バインディングを提供します selectionのデフォルト値は メトリックスであるため ワークアウトが開始されると メトリックスビューが表示されます TabViewを追加しましょう

    TabViewの選択パラメータは selection変数への バインディングを使用します テキストビューは 作成されるまで 各ビューのプレースホルダーです 各ビューには選択できる ようにタグが付いています Resumeをクリックして SessionPagingViewが どのように見えるかを確認します

    ライブプレビューをクリックすると ビューを切り替えて表示できます

    SessionPagingViewの selection変数のデフォルト値が metricsであるため Metricsテキストビューが 最初に表示されていることに 注意してください 左にスワイプするとコントロール テキストビューが表示されます 右端までスワイプすると プレイテキストビューが 表示されます 素晴らしい! ワークアウトの実行中は ライブメトリックスが表示されます セッションがランニングなどの 移動を呼び出す場合 特に重要な情報を読みやすいように Appは大きなフォントサイズを 使用しテキストを配置する 必要があります MetricsViewには 経過時間 アクティブエネルギー 現在の心拍数と 距離が表示されます HealthKitにはさらに多く のHKQuantityTypes が用意されています MetricsViewを 作成してみましょう ファイル>新規>ファイルの順に クリックします SwiftUIビューで 次へをクリックします これに「MetricsView」という 名付けて作成をクリック

    VStackは 4つのメトリックス Textビューが含まれます

    これらのTextビューをモデルに 接続するまでは Textビューにはデフォルト値 が設定されています

    黄色のforegroundColorと セミボールドのfontWeightを 指定して経過時間を焦点に しましょう

    アクティブエネルギーテキストビュー ではエネルギー単位キロカロリーの 既定値を使用して測定値が 作成されます Measurementでは単位を 省略した新しいフォーマットの 関数を使用します 使用量は消費したワークアウト エネルギーのワークアウトであり numberFormatには分数を トリミングするための fractionLengthがありません

    心拍数のテキストビューは fractionLengthゼロで フォーマットされたデフォルト値を 使用します フォーマットした文字列に 1分あたりの拍数の「bpm」を 追加します

    距離テキストビューは UnitLength.metersの デフォルト値を使用します 測定値は省略された単位で フォーマットされています usageはroadでロケールに 基づいて自然に進行する 帝国単位または メートル単位で表示されます

    丸みのあるデザインのタイトル monospacedDigits lowercaseSmallCapsの システムフォントを使用します

    メトリックスをリーディングエッジに 位置合わせしたいので VStackにmaxWidth infinityと リーディング位置合わせを持つ フレームビューモディファイアを 指定しました

    このVStackのコンテンツを 画面の一番下まで 拡張したいと考えています これを可能にするために 下のセーフエリアは無視します

    メトリックスをナビゲーションバーの タイトルに合わせたいので scenePadding()を 使用しました

    経過時間テキストビュー で経過時間を適切にフォーマットし 常時表示状態に基づいて サブセカンドを表示または 非表示にします これを行うには ElapsedTimeView カスタムElapsedTimeFormatterを 作成します ファイル>新規>ファイルの 順にクリックします SwiftUIビューで 次へをクリックします ElapsedTimeView 名前として作成をクリックします

    ElapsedTimeView の経過時間は TimeIntervalであり デフォルトはゼロです showSubsecondsはBool型の 引数でデフォルトはtrueです timeFormatterは 以下で定義する ElapsedTimeFormatter 状態変数です

    ビューの本体には elapsedTimeを NSNumberにキャストする Textビューが含まれているので timeFormatterがそれを 使用できます Textビューのフォントは 半太字のfontWeightです showSubsecondsが変更すると timeFormatterの showSubseconds変数も 変更されます

    ElapsedTimeFormatterは カスタムフォーマッタとして DateComponentsFormatterを 使用しています 経過時間には分と秒を表示し ゼロをパディングします showSubseconds変数は サブ秒が表示されているかどうかを 示します

    オプションのStringを返す value関数の文字列を オーバーライドします 最初のガードは値が TimeIntervalであることを確認

    2つ目のガードは componentsFormatterが文字列を 返すようにします

    showSubsecondsがtrueの場合 次のようにしてサブ秒を計算します truncatedRemainderを 1で除算し100を乗算して 取得します ローカライズされた decimalSeparatorを 使用しサブ秒を追加した formattedStringを返します

    showSubsecondsが falseの場合 サブ秒を指定せずに formattedStringを返す 再生をクリックすると ElapsedTimeViewを確認

    なかなかよさそうですね 分はコロンの左側にゼロが パディングされています 秒にはコロンの右側に ゼロがパディングされます 小数点以下の秒数が表示されます ElapsedTimeViewを MetricsViewに追加 MetricsViewを クリックします

    経過時間Textビューを ElapsedTimeViewに 置き換えます

    MetricsViewを プレビューします 見た目は最高です

    ControlsViewには 進行中のセッションを コントロールする 終了 一時停止 再生などの ボタンがあります 終了ボタンをタップすると ワークアウトの概要が表示されます 一時停止ボタンをタップすると ワークアウトが一時停止し MetricsViewが 表示されます ControlsViewを 作成します ファイル>新規>ファイルの 順にクリックします SwiftUIビュー

    「Controls View」という名付け 作成をクリックします

    終了と一時停止ボタンを追加します

    HStackには2つの VStackがあり 各VStackには1つのボタンと 1つのテキストビューがあります

    終了ボタンのラベルはsystemNameが 「xmark」のイメージです ボタンは赤色で表示され title2フォントを使用して シンボルのサイズが大きくなります 下のテキストビューに「終了」という 文字列があります

    一時停止ボタンはsystemNameが 「pause」のイメージを使用します tintは黄色です 下のテキストビューには 「Pause」という文字列があります 再開ボタンをクリックして ControlsViewをプレビュー

    すてきですね

    NowPlayingViewは ワークアウトが行われている間 のメディア再生コントロールを 提供します メディアを再生中の サードパーティーApp のコントロールも含まれます NowPlayingViewを 追加しましょう SessionPagingViewを選択

    NowPlayingViewは WatchKitが提供しています WatchKitを インポートします

    テキストビューをControlsView MetricsViewおよび NowPlayingViewに 置換しましょう

    NowPlayingViewはWatchKitが 提供するSwiftUIビューです シンプルです 再開をクリックして SessionPagingViewを確認します

    プレビューでは MetricsView が表示されます 左にスワイプして ControlsViewを表示します

    右端までスワイプ...

    ... NowPlayingViewを 確認します

    StartViewに戻り NavigationLinkの デスティネーションを SessionPagingViewに変更します StartViewを選択します

    デスティネーションを SessionPagingViewに更新します

    サマリー画面ではワークアウトが 完了したことが確認され 記録された情報が表示されます 現在の進捗状況を簡単に 確認できるように アクティビティリングを追加して サマリーを拡張します Summaryビューを作成します ファイル>新規>ファイルの 順にクリックします SwiftUIビューで 次へをクリックします これに「SummaryView」という 名前を付け 作成をクリックします

    メトリックスとその値を記述する カスタムSummaryMetricViewを 作成します

    SummaryMetricViewは メトリックスを説明するタイトル とメトリックスの値の文字列 を受け取ります

    Bodyには2つのText ビューとディバイダが含まれます メトリックス値を示す テキストビューでは 丸いデザインのタイトル システムフォントと lowercaseSmallCapsが 使用されます foregroundColorとして accentColorを使用します SummaryViewの ワークアウト期間 フォーマッタを作成しましょう

    durationFormatterは 時間 分 秒をコロンと ゼロで区切って表示する DateComponentsFormatter です SummaryMetricViews とDoneボタンを SummaryViewのサマリーに 追加します

    ScrollViewとVStackには 4つのSummaryMetricViewsと 完了ボタンがあります

    合計時間テキストビューは durationFormatterを 使用して時間 分 秒をコロンで 区切って表示します

    合計距離 SummaryMetricViewは 省略単位を使用して書式設定された 既定値で Measurementを 使用します usageはroadで ロケール に基づいて自然に進行 帝国単位またはメートル単位で 表示されます

    トータルエネルギー SummaryMetricViewは デフォルト値とキロカロリーの エネルギー単位で測定します 省略単位を使用して フォーマットされます 使用量はワークアウトエネルギーの ワークアウトであり numberFormatの精度は fractionLengthゼロです

    平均心拍数 SummaryMetricViewは 数値の精度fractionLengthがゼロで フォーマットされたデフォルト値を 使用して 1分あたりの拍数値に 「bpm」を付加します SummaryMetricViewsの 実際の ワークアウト値は後で提供します テキストビューと区切り線を ナビゲーションバーのタイトルに 揃えるため VStackでは.scenePadding ()を 使用しました

    navigationTitleは 「Summary」になり ナビゲーションバーに インラインで表示されます 次に ワークアウトのサマリーに アクティビティリングを追加します ファイル>新規>ファイルの 順にクリックします Swift File 次へを クリックします 「ActivityRingsView」という名付け 作成をクリックします

    HealthKitをインポートして HKHealthStoreにアクセスします SwiftUIをインポート WKInter- faceObjectRepresentableに登録

    ActivityRingsView 構造体は WKInterfaceObjectRepresentableに 準拠しています 初期化時にhealthStore定数が 割り当てられます

    プロトコルに準拠するには 次の2つの機能が必要です makeWKInterfaceObjectと updateWKInterfaceObject

    makeWKInterfaceObject内で WKInterfaceActivityRing である activityRingsObjectを 宣言します

    次に HKActivitySummaryQueryの 述部を作成して 今日の 日付コンポーネントを使用します 次にクエリを作成して その結果を処理して メインキューの activityRingsObjectに アクティビティサマリーを 設定します

    次にHKHealthStoreで クエリを実行します 最後にactivityRingsObject に戻します ActivityRingsViewを SummaryViewに追加しましょう SummaryViewを クリックしてください

    HealthKitをインポートして HKHealthStoreにアクセスします

    次にDoneボタンの上に Textビューと ActivityRingsViewを 追加します

    Textビューと ActivityRingsViewsを フレーム幅と高さ50で 追加しました ここでHKHealthStoreを 作成します 後で再利用します SummaryViewをプレビューします 再生をクリックします

    ライブプレビューをクリックすると スクロールできるようになります

    SummaryMetricViews

    Activity Rings Doneボタンを それぞれ確認してください HealthKitの統合について 説明します HealthKitにはワークアウト中の フィットネス活動をトラッキングする 機能が組み込まれており そのワークアウトを HealthKitに 保存しましょう これにより 開発者としての時間が節約され 顧客はすべてのワークアウトを 1つの場所に保存できます HKWorkoutSessionは デバイスのセンサーをデータ収集の ために準備するので カロリーや心拍数などの ワークアウトに関連するデータを 正確に収集することができます また ワークアウトがアクティブ なときにアプリケーションを バックグラウンドで 実行することもできます HKLiveWorkoutBuilderはHKWorkout オブジェクトを作成して保存します サンプルとイベントが自動的に 収集されます 詳しくは 「ワークアウトに対する 新しいアプローチ」 セッションをチェックして みてください Appのデータフローを 見てみましょう WorkoutManagerは HealthKitとの インターフェースを担当します HKWorkoutSessionと インターフェイスして ワークアウトを 開始 一時停止 終了します HKLiveWorkoutBuilderと インターフェースして ワークアウトのサンプルを受け取り そのデータをビューに 提供します WorkoutManagerは 環境オブジェクトになります 環境オブジェクトは 監視可能なオブジェクトが 変更されるたびに 現在のビューを無効にします MyWorkoutsAppのNavigationViewに WorkoutManager環境オブジェクトを 割り当てます 環境オブジェクトは WorkoutManagerを NavigationViewの ビュー階層内のビューに伝播します その後ビューは @EnvironmentObjectを宣言して 環境内のWorkoutManagerへの アクセス権を取得します WorkoutManagerを 作成しましょう ファイル>新規>ファイルの順に クリックします Swift File 次へをクリックします 「WorkoutManager」という名付けて 作成をクリックします

    WorkoutManagerが HealthKitのAPIにアクセス できるようにHealthKitを インポートします

    次にObservableObject プロトコルに準拠する NSObjectである WorkoutManagerクラスを定義します 全ビューにWorkoutManagerへの アクセス権を付与します これを行うには MyWorkoutsAppの NavigationViewで WorkoutManagerを 環境オブジェクトとして 割り当てます MyWorkoutsAppを 選択します

    workoutManagerを StateObjectとして追加します

    NavigationViewに environmentObject ビューモディファイアを追加します

    NavigationViewに environmentObjectが 割り当てられると ビュー階層内のビューに environmentObjectが 自動的に転送されます ナビゲーションモデルを 設定してみましょう WorkoutManagerを 選択します

    WorkoutManagerは 選択されたワークアウトを管理して これはオプションの HKWorkoutActivityTypeです

    選択したワークアウトを追跡する selectedWorkout変数を 追加しました StartViewの NavigationLinkは WorkoutManagerの selectedWorkoutに 選択をバインドする必要があります StartViewを選択します

    StartViewにworkoutManager EnvironmentObjectを追加します

    タグと選択を使用して NavigationLinkを更新します

    tagは workoutTypeです selectionは workoutManagerの selectedWorkoutへの バインディングです これでワークアウトが タップされると workoutManagerの selectedWorkoutが更新されます ここでワークアウトが選択された時 HKWorkoutSessionと HKLiveWorkoutBuilderを 開始します WorkoutManagerを 選択します

    HKHealthStore HKWorkoutSession 及びHKLiveWorkoutBuilderを 追加します 次startWorkout関数を作成して ワークアウトを開始します

    startWorkout関数は workoutTypeパラメータを取得 workoutTypeを使用して HKWorkoutConfigurationを作成 私たちのAppでは すべてのワークアウトが屋外です 場所の種類によって HKWorkoutSessionと HKLiveWorkoutBuilderが 動作します たとえば 屋外サイクリング アクティビティは正確な 位置データを生成しますが 屋内サイクリングアクティビティは 生成しません healthStoreと設定を 使用して HKWorkoutSessionを 作成します

    セッションに関連した WorkoutBuilderにビルダを割当 これはスローしたエラーを処理する ためdo-catchブロックで実行された

    healthStoreと workoutConfigurationを使用して ビルダーのdataSourceを HKLiveWorkoutDataSourceに割当 HKLiveWorkoutDataSourceは アクティブなワークアウト セッションからライブデータを 自動的に生成します

    startDateを作成します セッションでstartActivityを コールしてビルダーで beginCollectionを コールします selectedWorkoutが 変更されたら startWorkoutを 呼び出します

    selectedWorkoutには nilを指定できます selectedWorkoutがnilではない 場合にのみ guard文を使用して startWorkoutを 呼び出します Appがワークアウトセッションを 作成する前に HealthKitを設定し Appが 使用する予定のヘルスケアデータを 読み取って共有するための承認を リクエストする必要があります 許可をリクエストする機能を 追加しましょう

    ワークアウトセッションでは ワークアウトタイプ共有するため アクセス許可を リクエストする必要があります

    また セッションの一部として Apple Watchによって 自動的に記録されたデータ型を 読み取ることもできます

    Activity Ringsサマリーを 読む権限も必要です

    次にhealthStoreで requestAuthorizationを呼出 ビューが表示されたら HealthKitからStartView リクエストの承認を取得しましょう StartViewを クリックします

    表示されると workoutManagerの requestAuthorization関数が 呼ばれます 拡張のために HealthKitを有効にします 下記MyWorkouts プロジェクトファイルを選択...

    ... MyWorkouts WatchKit Extension Signing & Capabilities Add Capabilityを選択し 下に行き HealthKitを選択します

    アクティブワークアウトセッション ありのAppはバックグラウンドで 実行できるので WatchKit Extensionに バックグラウンドモード機能を 追加する必要があります ワークアウトセッションには ワークアウト処理バックグラウンド モードをリクエストします 機能の追加 バックグラウンドモードの 順に選択します ワークアウト処理を選択します WatchKit Extensionの Info.plistファイルに 使用方法の説明を追加する 必要があります Info.plistを選択します

    最後の行を選択し Returnキーを押します

    NSHealth ShareUsageDescription キーを使用します

    アプリケーションで要求された データを読み取る理由を説明します Returnキーを押します

    NSHealth UpdateUsageDescription キーを使用します

    Appが作成する予定の データを説明します

    Appを構築して実行し AppがHealthKitに許可を リクエストするのを確認します 実行をクリックします

    AppはHealthKit認証を リクエストしています 下にスクロールして Reviewをクリックします

    以下のすべてのリクエストデータを 選択します

    Appがワークアウトの共有を リクエストしているのを確認します 説明を参照してください 次をタップします アプリケーションが読み取り アクセスをリクエストしています 以下のすべてのリクエストデータを 選択します Appが読取アクセスを リクエストしたデータ型を確認

    説明を参照してください 完了をタップします

    これでワークアウトセッションを 開始できるので HKWorkoutSessionをコントロール 必要があります WorkoutManagerを 選択します

    セッションステートコントロール ロジックを追加しましょう

    セッションが実行中の場合 「running」という名前の @Published変数が 追跡されます

    一時停止と再開機能はセッションを 一時停止および再開させます togglePause関数は セッションが実行中かどうかに 基づいて セッションを一時停止 または再開させます

    endWorkout関数は セッションを終了させます WorkoutManagerを HKWorkoutSessionDelegate に拡張して セッション状態の変更を待機します

    セッション状態が変更されるたびに workoutSession didChangeTotoState fromState Date関数が 呼び出されます

    実行中の変数は toStateが実行中かどうかに 基づいて更新され UI更新のためにメインキュー に送信されます

    セッションが終了に移行したら 終了日を指定してビルダーの endCollection を呼び出し ワークアウトサンプルの 収集を停止します endCollection完了後 finishWorkoutを呼び 出してHKWorkoutを Healthデータベースに保存 HKWorkoutSession のデリゲートとして WorkoutManager を必ず割り当ててください

    では ControlsView にセッションの一時停止 再開 終了をさせてみましょう ControlsViewを 選択します

    workoutManagerを EnvironmentObject として追加しビューがセッションを コントロールできるようにします

    終了ボタンで workoutManagerの endWorkoutを 呼び出します

    一時停止/再生ボタンは セッションを一時停止または再生し その画像とテキストをセッション 状態に応じて更新します

    ボタンの動作は workoutManagerの togglePause関数を呼び出し セッションを一時停止や再生します

    ボタンの画像のsystemNameは workoutManagerの実行変数に 基づいて「一時停止」または「再生」の いずれかになります workoutManagerの実行中の変数に 基づいてボタンの下に表示される テキストは「一時停止」または 「再生」のいずれかを表示します SessionPagingViewを更新して ワークアウト名を ナビゲーションバーに 表示してみましょう SessionPagingViewを 選択します

    SessionPagingViewは WorkoutManager環境変数に アクセスする必要があるので それを追加しましょう

    ナビゲーションバーを 設定しましょう ナビゲーションタイトルは WorkoutManagerの selectedWorkoutの名前です ナビゲーションバーの戻る ボタンは隠されています ワークアウト中にStartViewに 戻ってしまうことを 防ぐためです NowPlayingViewが 表示されているとき ナビゲーションバーを 非表示にしたいと思います ワークアウトを一時停止や再生する ときに MetricsViewに移動する 必要はありません これを行うには onChangeビューモディファイアを 追加します

    WorkoutManager の実行中の公開変数が変化すると displayMetricsView 関数が呼び出されます displayMetricsView は選択状態の変数を metrics withAnimation に設定します ワークアウトが終了したところで SummaryViewの表示と 非表示の機能を追加してみましょう WorkoutManagerを クリックします

    「showingSummaryView」という名前の 公開された変数を追加します この変数のブール値の デフォルトはfalseです

    この変数は Appの ナビゲーションビュー上での シートの選択に対する バインディングを提供します endWorkoutでshowingSummaryViewを trueに設定します

    SummaryViewを SheetとしてMyWorkoutsAppの NavigationViewに 追加してみましょう MyWorkoutsAppを クリックします

    シートビューモディファイヤを NavigationViewに追加します

    isPresentedパラメータは workoutManagerの showingSummaryViewへの バインディングです シートの内容は SummaryViewです SummaryViewには シートを閉じる機能を 追加しましょう SummaryViewを クリックします

    dismise DnEnvironment変数を 追加します

    Doneボタンのアクションで dismiss()を呼び出します

    セッションの開始と終了し SummaryViewを表示するため アプリケーションを実行しましょう 停止をクリックすると 前の実行を停止します

    実行をクリックします

    ランニングの ワークアウトをタップします

    デフォルトのメトリックス値は セッション中や サマリーにも表示されます それは後で設定します 左にスワイプします 一時停止をタップします MetricsViewが表示されることを 注意してください 左にスワイプします

    ボタンに「再生」と表示されます 終了をタップします

    ワークアウトサマリーが シートとして表示されます スクロールダウンしてください 完了をタップします シートが解除され となり StartViewが表示されます MetricsViewと SummaryViewに 実際のワークアウトの 測定値を表示させてみましょう WorkoutManagerはMetricsViewと SummaryViewが監視できる 公開したワークアウトメトリックを 公開します WorkoutManagerを 選択します

    Published metric変数を WorkoutManagerに追加します

    averageHeartRateが SummaryViewで使われます heartRate activeEnergy とdistanceは MetricsViewによって 測定されます WorkoutManagerは HKLiveWorkoutBuilderDelegate としてビルダに追加ワークアウト サンプルを調べる必要があります 今すぐやりましょう まずビルダのデリゲートを WorkoutManagerとして 割り当てます

    ここでWorkoutManagerを HKLiveWorkoutBuilderDelegate プロトコルに一致させます

    HKLiveWorkoutBuilderDelegate プロトコルに一致するように WorkoutManagerを拡張します

    workoutBuilderDidCollectEventは ビルダがイベントを 収集するたびに呼び出されます この関数はApp用に 空のままにします

    workoutBuilder didCollectDataOf collectedTypesはビルダが新しい サンプルを収集する毎に呼出します

    collectedTypesの各タイプを 繰り返し処理します ガード収集型がHKQuantityType であると確認します 統計はその数量タイプの ビルダから読み込まれます updateForStatistics -- 近いうちに作成する関数が 呼び出され 公開された メトリックス値が更新されます updateForStatistics関数を 作成しましょう

    updateForStatisticsは随意の HKStatisticsオブジェクトを取得 統計がnilの場合 ガードは早期に返します

    メトリックス更新を メインキューに非同期で 送信します 量のタイプのごとに 切り替えます heartRateの場合は1分あたりの 拍数を知りたいのでカウントの HKUnitを分単位のHKUnitで 割ったものを使います 1分あたりの心拍数の mostRrecentQuantity doubleValueとして heartRateを割り当てます。 1分あたりの拍数の statistics.averageQuantityの doubleValueとして averageHeartRateを割り当てます

    activeEnergyBurned quantityTypeには kilocalorie energyUnitを 使用する activeEnergyをsumQuantityの doubleValueとして energyUnitに 割り当てます

    ウォーキング ランニング サイクリングの距離では meterUnitのsumQuantityの doubleValueを取得します では MetricsViewに WorkoutManagerから メトリックス値を使います MetricsViewを 選択します

    workoutManagerを 環境変数として追加します

    WorkoutManagerからの メトリックス値を使用するよう ビューを更新しましょう

    ElapsedTimeViewは workoutManagerの ビルダのelapsedTimeを使います

    activeEnergy Text ビューのMeasurementは workoutManagerの activeEnergyを使用します

    heartRate Textビューは workoutManagerのheartRateを使用

    距離テキストビューの測定では workoutManagerの距離が 使用されます

    ビルダの経過時間変数は 公開されていないため 現在のビューはビルダの elapsedTimeが 更新されても更新しません VStackをTimelineViewで ラップすることができます

    TimelineViewは 今年の新機能です TimelineViewは予定に 従い時間の経過と共に更新されます watchOS Appが 常時表示状態をサポートします TimelineViewsは 常時表示コンテキストへの 変更をビューに認識させます 詳しくは 「watchOS 8の新機能」と 「SwiftUIの新機能」のセッションを ご覧ください App状態は アクティブな状態か 常時表示の状態か アクティブなワークアウト セッションを持つAppは 常時表示状態で 最大で1秒に1回更新できます これは MetricsViewが 常時表示状態の サブ秒を非表示にする 必要があることを意味します 常時表示の状態では ページインジケータコントロールを 非表示にして表示を簡素化するなど 他のデザイン上の配慮が必要です TimelineViewには 常時表示コンテキストで 指定されたTimelineScheduleModeに 基づいて間隔を変更するカスタム TimelineScheduleが 必要です それではカスタムの TimelineScheduleを作成しましょう

    MetricsTimelineScheduleは スケジュールを開始する時期を 示すstartDateを 持っています そのイニシャライザ は startDateを受け取ります

    MetricsTimelineScheduleは エントリー機能を実装しています PeriodicTimelineSchedule のエントリ この関数はstartDateを 使用して PeriodicTimelineScheduleを 作成します TimelineScheduleModeによって 間隔が決定されます TimelineScheduleModeが lowFrequencyの場合 TimelineScheduleの 間隔は1秒です TimelineScheduleModeが 正常な場合 間隔は30回/秒です VStackをTimelineView で表示してみましょう

    TimelineViewはビルダの startDateを使用して MetricsTimelineScheduleを 使用します ElapsedTimeViewの showSubsecondsはTimelineViewの context.cadenceによって 決定します ケイデンスがライブのときは 秒単位で表示されます それ以外の場合サブ秒は 常時表示状態で非表示になります アプリケーションを実行して ワークアウト中に更新される メトリックスを確認してみましょう 現在の実行を停止するには 停止をクリックします 実行をクリックします

    ワークアウトの実行を タップします 経過時間に注目 が増加している ことに注意してください watchOSシミュレーターは リアルタイムの ワークアウトサンプルを 自動的に収集します カロリーが発生しています 心拍数が更新されています 距離が蓄積されていく シミュレータ上でLock ボタンをクリックして 常時表示状態を 試してみましょう

    サブ秒は非表示で メトリックスは1秒に1回しか 更新されないことに 注意してください ロック解除ボタンをクリックすると アクティブな状態に戻ります

    左にスワイプして ワークアウトを終了します

    SummaryViewにはまだ 実際のHKWorkout値が必要 今すぐやりましょう まずSummaryViewで使用 するHKWorkoutを WorkoutManagerに 追加します WorkoutManagerを 選択します

    HKWorkout公開された変数を 追加します

    ビルダがワークアウトの 保存を完了したらビルダの finishWorkout関数が 完了した時点でワークアウトを WorkoutManagerに 割り当てます

    この課題をUIの更新のための メインキューです SummaryViewが閉じた時 モデルを再設定します resetWorkout関数を作成して それをします

    resetWorkout関数は すべてのモデル変数を 初期状態にリセットします サマリーが終了したら resetWorkoutを呼出します showingSummaryViewの didSetで行います

    SummaryViewを 表示する前に ワークアウトの保存中に ワークアウトが終了したときに 進行状況ビューを表示します SummaryViewに アクセスしてみましょう SummaryViewを クリックします

    まずSummaryViewにworkoutManagerEnvironmentObjectを 追加します

    ビルダがワークアウトの保存を 終了したときに workoutManagerに HKWorkoutが割り当てられ ProgressViewを 表示します

    workoutManagerのワークアウトが nilの場合は 「ワークアウトを保存しています」 というテキストとともに ProgressViewを表示し ナビゲーションバーを 非表示にします

    WorkoutManagerの HKHealthStoreを 使う為ActivityRingsView もアップデートしました HKHealthStore は1つのAppに1つだけ必要です HKWorkoutの値を 使用するように SummaryMetricViewsを 更新します

    合計時間メトリックビューは ワークアウト期間を使用します

    totalDistance メトリックビューは ワークアウトの合計距離を 使用します

    Total Energy メトリックビューは ワークアウトの totalEnergyBurnedを使用します

    平均心拍数メトリックビューは workoutManagerの averageHeartRateを 使用します 平均心拍数を保存しておきたい 場合は ワークアウトを保存する前に メタデータとして ビルダに追加することができます SessionPagingViewを 常時オン状態に 反応するように更新してみましょう SessionPagingViewを 選択します

    isLuminanceReduced環境変数を 追加します

    常時表示状態では TabViewの ページインジケータを非表示にし MetricsViewが 表示されていることを確認します

    isLuminanceReducedに基づいて tabViewStyleのindexDisplayModeを neverまたはautomaticに 設定しました isLuminanceReducedが 変更されたら displayMetricsView関数を 呼び出して MetricsViewを 表示します シミュレーターでAppを 実行して試してみましょう 停止をクリックして 最後の実行を停止します 実行をクリックします

    ワークアウト実行を選択します メトリックスがビルダからライブ更新 されているので注意してください 左にスワイプします 一時停止をタップします ワークアウトが一時停止のため 測定値の更新が止まっていることに 注意してください 左にスワイプします 再開をタップします メトリックの更新が再開されます 右にスワイプすると NowPlayingViewを確認します 左にスワイプします ロックをクリックして 常時表示状態をトリガーします サブセカンドが非表示になり ページコントロールインジケータが 非表示になっていることに 注意してください。 ロック解除をクリックすると アクティブな状態になります

    左にスワイプして 終了をタップします

    ワークアウトが保存されます サマリーが表示されます 下にスクロールして 各メトリックスを表示します

    アクティビティリングは エネルギー量 運動時間 および待機時間に基づいて 生成されます 完了をタップします

    最初のビューに戻り 次のワークアウトの準備が 整いました

    SwiftUIを使って 常時表示状態をサポートする HealthKitと統合した完全に 機能するワークアウトAppを 実装するのがいかに簡単かを ご覧いただきました 次はどんな良いワークアウトAppを 作ってくれるのか楽しみですね ♪

    • 3:17 - StartView - import HealthKit

      import HealthKit
    • 3:25 - StartView - workoutTypes

      var workoutTypes: [HKWorkoutActivityType] = [.cycling, .running, .walking]
    • 3:26 - StartView - HKWorkoutActivityType identifiable and name

      extension HKWorkoutActivityType: Identifiable {
          public var id: UInt {
              rawValue
          }
      
          var name: String {
              switch self {
              case .running:
                  return "Run"
              case .cycling:
                  return "Bike"
              case .walking:
                  return "Walk"
              default:
                  return ""
              }
          }
      }
    • 4:22 - StartView - body

      List(workoutTypes) { workoutType in
          NavigationLink(
              workoutType.name,
              destination: Text(workoutType.name)
          ).padding(
              EdgeInsets(top: 15, leading: 5, bottom: 15, trailing: 5)
          )
      }
      .listStyle(.carousel)
      .navigationBarTitle("Workouts")
    • 6:55 - SessionPagingView - Tab enum and selection

      @State private var selection: Tab = .metrics
      
      enum Tab {
          case controls, metrics, nowPlaying
      }
    • 7:20 - SessionPagingView - TabView

      TabView(selection: $selection) {
          Text("Controls").tag(Tab.controls)
          Text("Metrics").tag(Tab.metrics)
          Text("Now Playing").tag(Tab.nowPlaying)
      }
    • 9:02 - MetricsView - VStack and TextViews

      VStack(alignment: .leading) {
          Text("03:15.23")
              .foregroundColor(Color.yellow)
              .fontWeight(.semibold)
          Text(
              Measurement(
                  value: 47,
                  unit: UnitEnergy.kilocalories
              ).formatted(
                  .measurement(
                      width: .abbreviated,
                      usage: .workout,
                      numberFormat: .numeric(precision: .fractionLength(0))
                  )
              )
          )
          Text(
              153.formatted(
                  .number.precision(.fractionLength(0))
              )
              + " bpm"
          )
          Text(
              Measurement(
                  value: 515,
                  unit: UnitLength.meters
              ).formatted(
                  .measurement(
                      width: .abbreviated,
                      usage: .road
                  )
              )
          )
      }
      .font(.system(.title, design: .rounded)
              .monospacedDigit()
              .lowercaseSmallCaps()
      )
      .frame(maxWidth: .infinity, alignment: .leading)
      .ignoresSafeArea(edges: .bottom)
      .scenePadding()
    • 11:42 - ElapsedTimeView - ElapsedTimeView and ElapsedTimeFormatter

      struct ElapsedTimeView: View {
          var elapsedTime: TimeInterval = 0
          var showSubseconds: Bool = true
          @State private var timeFormatter = ElapsedTimeFormatter()
      
          var body: some View {
              Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
                  .fontWeight(.semibold)
                  .onChange(of: showSubseconds) {
                      timeFormatter.showSubseconds = $0
                  }
          }
      }
      
      class ElapsedTimeFormatter: Formatter {
          let componentsFormatter: DateComponentsFormatter = {
              let formatter = DateComponentsFormatter()
              formatter.allowedUnits = [.minute, .second]
              formatter.zeroFormattingBehavior = .pad
              return formatter
          }()
          var showSubseconds = true
      
          override func string(for value: Any?) -> String? {
              guard let time = value as? TimeInterval else {
                  return nil
              }
      
              guard let formattedString = componentsFormatter.string(from: time) else {
                  return nil
              }
      
              if showSubseconds {
                  let hundredths = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
                  let decimalSeparator = Locale.current.decimalSeparator ?? "."
                  return String(format: "%@%@%0.2d", formattedString, decimalSeparator, hundredths)
              }
      
              return formattedString
          }
      }
    • 13:56 - MetricsView - replace TextView with ElapsedTimeView

      ElapsedTimeView(
          elapsedTime: 3 * 60 + 15.24,
          showSubseconds: true
      ).foregroundColor(Color.yellow)
    • 14:47 - ControlsView - Stacks, Buttons and TextViews

      HStack {
          VStack {
              Button {
              } label: {
                  Image(systemName: "xmark")
              }
              .tint(Color.red)
              .font(.title2)
              Text("End")
          }
          VStack {
              Button {
              } label: {
                  Image(systemName: "pause")
              }
              .tint(Color.yellow)
              .font(.title2)
              Text("Pause")
          }
      }
    • 16:05 - SessionPagingView - import WatchKit

      import WatchKit
    • 16:09 - SessionPagingView - TabView using actual views

      ControlsView().tag(Tab.controls)
      MetricsView().tag(Tab.metrics)
      NowPlayingView().tag(Tab.nowPlaying)
    • 17:08 - StartView - NavigationLink to use SessionPagingView

      destination: SessionPagingView()
    • 17:50 - SummaryView - SummaryMetricView

      struct SummaryMetricView: View {
          var title: String
          var value: String
      
          var body: some View {
              Text(title)
              Text(value)
                  .font(.system(.title2, design: .rounded)
                          .lowercaseSmallCaps()
                  )
                  .foregroundColor(.accentColor)
              Divider()
          }
      }
    • 18:27 - SummaryView - durationFormatter

      @State private var durationFormatter: DateComponentsFormatter = {
          let formatter = DateComponentsFormatter()
          formatter.allowedUnits = [.hour, .minute, .second]
          formatter.zeroFormattingBehavior = .pad
          return formatter
      }()
    • 18:45 - SummaryView - body

      ScrollView(.vertical) {
          VStack(alignment: .leading) {
              SummaryMetricView(
                  title: "Total Time",
                  value: durationFormatter.string(from: 30 * 60 + 15) ?? ""
              ).accentColor(Color.yellow)
              SummaryMetricView(
                  title: "Total Distance",
                  value: Measurement(
                      value: 1625,
                      unit: UnitLength.meters
                  ).formatted(
                      .measurement(
                          width: .abbreviated,
                          usage: .road
                      )
                  )
              ).accentColor(Color.green)
              SummaryMetricView(
                  title: "Total Energy",
                  value: Measurement(
                      value: 96,
                      unit: UnitEnergy.kilocalories
                  ).formatted(
                      .measurement(
                          width: .abbreviated,
                          usage: .workout,
                          numberFormat: .numeric(precision: .fractionLength(0))
                      )
                  )
              ).accentColor(Color.pink)
              SummaryMetricView(
                  title: "Avg. Heart Rate",
                  value: 143
                      .formatted(
                          .number.precision(.fractionLength(0))
                      )
                  + " bpm"
              ).accentColor(Color.red)
              Button("Done") {
              }
          }
          .scenePadding()
      }
      .navigationTitle("Summary")
      .navigationBarTitleDisplayMode(.inline)
    • 21:00 - ActivityRingsView

      import HealthKit
      import SwiftUI
      
      struct ActivityRingsView: WKInterfaceObjectRepresentable {
          let healthStore: HKHealthStore
      
          func makeWKInterfaceObject(context: Context) -> some WKInterfaceObject {
              let activityRingsObject = WKInterfaceActivityRing()
      
              let calendar = Calendar.current
              var components = calendar.dateComponents([.era, .year, .month, .day], from: Date())
              components.calendar = calendar
      
              let predicate = HKQuery.predicateForActivitySummary(with: components)
      
              let query = HKActivitySummaryQuery(predicate: predicate) { query, summaries, error in
                  DispatchQueue.main.async {
                      activityRingsObject.setActivitySummary(summaries?.first, animated: true)
                  }
              }
      
              healthStore.execute(query)
      
              return activityRingsObject
          }
      
          func updateWKInterfaceObject(_ wkInterfaceObject: WKInterfaceObjectType, context: Context) {
      
          }
      }
    • 22:15 - SummaryView - add ActivityRingsView

      Text("Activity Rings")
      ActivityRingsView(
          healthStore: HKHealthStore()
      ).frame(width: 50, height: 50)
    • 22:28 - SummaryView - import HealthKit

      import HealthKit
    • 25:22 - WorkoutManager

      import HealthKit
      
      class WorkoutManager: NSObject, ObservableObject {
      
      }
    • 25:53 - MyWorkoutsApp - add workoutManager @StateObject

      @StateObject var workoutManager = WorkoutManager()
    • 26:00 - MyWorkoutsApp - .environmentObject to NavigationView

      .environmentObject(workoutManager)
    • 26:25 - WorkoutManager - selectedWorkout

      var selectedWorkout: HKWorkoutActivityType?
    • 26:49 - StartView - add workoutManager

      @EnvironmentObject var workoutManager: WorkoutManager
    • 26:56 - StartView - Add tag and selection to NavigationLink

      ,
      tag: workoutType,
      selection: $workoutManager.selectedWorkout
    • 27:32 - WorkoutManager - Add healthStore, session, builder

      let healthStore = HKHealthStore()
      var session: HKWorkoutSession?
      var builder: HKLiveWorkoutBuilder?
    • 27:42 - WorkoutManager - startWorkout(workoutType:)

      func startWorkout(workoutType: HKWorkoutActivityType) {
          let configuration = HKWorkoutConfiguration()
          configuration.activityType = workoutType
          configuration.locationType = .outdoor
      
          do {
              session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
              builder = session?.associatedWorkoutBuilder()
          } catch {
              // Handle any exceptions.
              return
          }
      
          builder?.dataSource = HKLiveWorkoutDataSource(
              healthStore: healthStore,
              workoutConfiguration: configuration
          )
      
          // Start the workout session and begin data collection.
          let startDate = Date()
          session?.startActivity(with: startDate)
          builder?.beginCollection(withStart: startDate) { (success, error) in
              // The workout has started.
          }
      }
    • 29:06 - WorkoutManager - selectedWorkout didSet

      {
          didSet {
              guard let selectedWorkout = selectedWorkout else { return }
              startWorkout(workoutType: selectedWorkout)
          }
      }
    • 29:35 - WorkoutManager - requestAuthorization from HealthKit

      // Request authorization to access HealthKit.
      func requestAuthorization() {
          // The quantity type to write to the health store.
          let typesToShare: Set = [
              HKQuantityType.workoutType()
          ]
      
          // The quantity types to read from the health store.
          let typesToRead: Set = [
              HKQuantityType.quantityType(forIdentifier: .heartRate)!,
              HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
              HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
              HKQuantityType.quantityType(forIdentifier: .distanceCycling)!,
              HKObjectType.activitySummaryType()
          ]
      
          // Request authorization for those quantity types.
          healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
              // Handle error.
          }
      }
    • 30:20 - StartView - requestAuthorization onAppear

      .onAppear {
          workoutManager.requestAuthorization()
      }
    • 31:30 - Privacy - Health Share Usage Description - Key

      NSHealthShareUsageDescription
    • 31:38 - Privacy - Health Share Usage Description - Value

      Your workout related data will be used to display your saved workouts in MyWorkouts.
    • 31:47 - Privacy - Health Update Usage Description - Key

      NSHealthUpdateUsageDescription
    • 31:54 - Privacy - Health Update Usage Description - Value

      Workouts tracked by MyWorkouts on Apple Watch will be saved to HealthKit.
    • 33:29 - WorkoutManager - session state control

      // MARK: - State Control
      
      // The workout session state.
      @Published var running = false
      
      func pause() {
          session?.pause()
      }
      
      func resume() {
          session?.resume()
      }
      
      func togglePause() {
          if running == true {
              pause()
          } else {
              resume()
          }
      }
      
      func endWorkout() {
          session?.end()
      }
    • 34:11 - WorkoutManager - HKWorkoutSessionDelegate

      // MARK: - HKWorkoutSessionDelegate
      extension WorkoutManager: HKWorkoutSessionDelegate {
          func workoutSession(_ workoutSession: HKWorkoutSession,
                              didChangeTo toState: HKWorkoutSessionState,
                              from fromState: HKWorkoutSessionState,
                              date: Date) {
              DispatchQueue.main.async {
                  self.running = toState == .running
              }
      
              // Wait for the session to transition states before ending the builder.
              if toState == .ended {
                  builder?.endCollection(withEnd: date) { (success, error) in
                      self.builder?.finishWorkout { (workout, error) in
                      }
                  }
              }
          }
      
          func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
      
          }
      }
    • 34:58 - WorkoutManager - assign HKWorkoutSessionDelegate in startWorkout()

      session?.delegate = self
    • 35:22 - ControlsView - workoutManager environmentObject

      @EnvironmentObject var workoutManager: WorkoutManager
    • 35:33 - ControlsView - End Button action

      workoutManager.endWorkout()
    • 35:43 - ControlsView - Pause / Resume Button and Text

      Button {
          workoutManager.togglePause()
      } label: {
          Image(systemName: workoutManager.running ? "pause" : "play")
      }
      .tint(Color.yellow)
      .font(.title2)
      Text(workoutManager.running ? "Pause" : "Resume")
    • 36:30 - SessionPagingView - add workoutManager environment variable

      @EnvironmentObject var workoutManager: WorkoutManager
    • 36:42 - SessionPagingView - navigationBar

      .navigationTitle(workoutManager.selectedWorkout?.name ?? "")
      .navigationBarBackButtonHidden(true)
      .navigationBarHidden(selection == .nowPlaying)
    • 37:10 - SessionPagingView - onChange of workoutManager.running

      .onChange(of: workoutManager.running) { _ in
              displayMetricsView()
          }
      }
      
      private func displayMetricsView() {
          withAnimation {
              selection = .metrics
          }
      }
    • 37:45 - WorkoutManager - showingSummaryView

      @Published var showingSummaryView: Bool = false {
          didSet {
              // Sheet dismissed
              if showingSummaryView == false {
                  selectedWorkout = nil
              }
          }
      }
    • 37:59 - WorkoutManager - showingSummaryView true in endWorkout

      showingSummaryView = true
    • 38:22 - MyWorkoutApp - add summaryView sheet to NavigationView

      .sheet(isPresented: $workoutManager.showingSummaryView) {
          SummaryView()
      }
    • 38:49 - SummaryView - add dismiss environment variable

      @Environment(\.dismiss) var dismiss
    • 38:58 - SummaryView - add dismiss() to done button

      dismiss()
    • 40:25 - WorkoutManager - Metric publishers

      // MARK: - Workout Metrics
      @Published var averageHeartRate: Double = 0
      @Published var heartRate: Double = 0
      @Published var activeEnergy: Double = 0
      @Published var distance: Double = 0
    • 40:48 - WorkoutManager - assigned as HKLiveWorkoutBuilderDelegate in startWorkout()

      builder?.delegate = self
    • 41:05 - WorkoutManager - add HKLiveWorkoutBuilderDelegate extension

      // MARK: - HKLiveWorkoutBuilderDelegate
      extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
          func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
          }
      
          func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
              for type in collectedTypes {
                  guard let quantityType = type as? HKQuantityType else { return }
      
                  let statistics = workoutBuilder.statistics(for: quantityType)
      
                  // Update the published values.
                  updateForStatistics(statistics)
              }
          }
      }
    • 42:01 - WorkoutManager - add updateForStatistics()

      func updateForStatistics(_ statistics: HKStatistics?) {
          guard let statistics = statistics else { return }
      
          DispatchQueue.main.async {
              switch statistics.quantityType {
              case HKQuantityType.quantityType(forIdentifier: .heartRate):
                  let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
                  self.heartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
                  self.averageHeartRate = statistics.averageQuantity()?.doubleValue(for: heartRateUnit) ?? 0
              case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
                  let energyUnit = HKUnit.kilocalorie()
                  self.activeEnergy = statistics.sumQuantity()?.doubleValue(for: energyUnit) ?? 0
              case HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning), HKQuantityType.quantityType(forIdentifier: .distanceCycling):
                  let meterUnit = HKUnit.meter()
                  self.distance = statistics.sumQuantity()?.doubleValue(for: meterUnit) ?? 0
              default:
                  return
              }
          }
      }
    • 43:25 - MetricsView - add workoutManager as environment variable to MetricsView

      @EnvironmentObject var workoutManager: WorkoutManager
    • 43:35 - MetricsView - VStack with Text bound to workoutManager variables

      VStack(alignment: .leading) {
          ElapsedTimeView(
              elapsedTime: workoutManager.builder?.elapsedTime ?? 0,
              showSubseconds: true
          ).foregroundColor(Color.yellow)
          Text(
              Measurement(
                  value: workoutManager.activeEnergy,
                  unit: UnitEnergy.kilocalories
              ).formatted(
                  .measurement(
                      width: .abbreviated,
                      usage: .workout,
                      numberFormat: .numeric(precision: .fractionLength(0))
                  )
              )
          )
          Text(
              workoutManager.heartRate
                  .formatted(
                      .number.precision(.fractionLength(0))
                  )
              + " bpm"
          )
          Text(
              Measurement(
                  value: workoutManager.distance,
                  unit: UnitLength.meters
              ).formatted(
                  .measurement(
                      width: .abbreviated,
                      usage: .road
                  )
              )
          )
      }
    • 45:51 - MetricsView - MetricsTimelineSchedule

      private struct MetricsTimelineSchedule: TimelineSchedule {
          var startDate: Date
      
          init(from startDate: Date) {
              self.startDate = startDate
          }
      
          func entries(from startDate: Date, mode: TimelineScheduleMode) -> PeriodicTimelineSchedule.Entries {
              PeriodicTimelineSchedule(
                  from: self.startDate,
                  by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0)
              ).entries(
                  from: startDate,
                  mode: mode
              )
          }
      }
    • 46:38 - MetricsView - TimelineView wrapping VStack

      TimelineView(
          MetricsTimelineSchedule(
              from: workoutManager.builder?.startDate ?? Date()
          )
      ) { context in
          VStack(alignment: .leading) {
              ElapsedTimeView(
                  elapsedTime: workoutManager.builder?.elapsedTime ?? 0,
                  showSubseconds: context.cadence == .live
              ).foregroundColor(Color.yellow)
              Text(
                  Measurement(
                      value: workoutManager.activeEnergy,
                      unit: UnitEnergy.kilocalories
                  ).formatted(
                      .measurement(
                          width: .abbreviated,
                          usage: .workout,
                          numberFormat: .numeric(precision: .fractionLength(0))
                      )
                  )
              )
              Text(
                  workoutManager.heartRate
                      .formatted(
                          .number.precision(.fractionLength(0))
                      )
                  + " bpm"
              )
              Text(
                  Measurement(
                      value: workoutManager.distance,
                      unit: UnitLength.meters
                  ).formatted(
                      .measurement(
                          width: .abbreviated,
                          usage: .road
                      )
                  )
              )
          }
          .font(.system(.title, design: .rounded)
                  .monospacedDigit()
                  .lowercaseSmallCaps()
          )
          .frame(maxWidth: .infinity, alignment: .leading)
          .ignoresSafeArea(edges: .bottom)
          .scenePadding()
      }
    • 48:23 - WorkoutManager - workout: HKWorkout added

      @Published var workout: HKWorkout?
    • 48:38 - WorkoutManager - assign HKWorkout in finishWorkout

      DispatchQueue.main.async {
          self.workout = workout
      }
    • 48:57 - WorkoutManager - resetWorkout()

      func resetWorkout() {
          selectedWorkout = nil
          builder = nil
          session = nil
          workout = nil
          activeEnergy = 0
          averageHeartRate = 0
          heartRate = 0
          distance = 0
      }
    • 49:21 - WorkoutManager - add resetWorkout to showingSummaryView didSet

      resetWorkout()
    • 49:48 - SummaryView - add workoutManager

      @EnvironmentObject var workoutManager: WorkoutManager
    • 50:06 - SummaryView - add ProgressView

      if workoutManager.workout == nil {
          ProgressView("Saving workout")
              .navigationBarHidden(true)
      } else {
          ScrollView(.vertical) {
              VStack(alignment: .leading) {
                  SummaryMetricView(
                      title: "Total Time",
                      value: durationFormatter.string(from: 30 * 60 + 15) ?? ""
                  ).accentColor(Color.yellow)
                  SummaryMetricView(
                      title: "Total Distance",
                      value: Measurement(
                          value: 1625,
                          unit: UnitLength.meters
                      ).formatted(
                          .measurement(
                              width: .abbreviated,
                              usage: .road
                          )
                      )
                  ).accentColor(Color.green)
                  SummaryMetricView(
                      title: "Total Calories",
                      value: Measurement(
                          value: 96,
                          unit: UnitEnergy.kilocalories
                      ).formatted(
                          .measurement(
                              width: .abbreviated,
                              usage: .workout,
                              numberFormat: .numeric(precision: .fractionLength(0))
                          )
                      )
                  ).accentColor(Color.pink)
                  SummaryMetricView(
                      title: "Avg. Heart Rate",
                      value: 143.formatted(
                          .number.precision(.fractionLength(0))
                      )
                      + " bpm"
                  )
                  Text("Activity Rings")
                  ActivityRingsView(healthStore: workoutManager.healthStore)
                      .frame(width: 50, height: 50)
                  Button("Done") {
                      dismiss()
                  }
              }
              .scenePadding()
          }
          .navigationTitle("Summary")
          .navigationBarTitleDisplayMode(.inline)
      }
    • 50:43 - SummaryView - SummaryMetricViews using HKWorkout values

      SummaryMetricView(
          title: "Total Time",
          value: durationFormatter
              .string(from: workoutManager.workout?.duration ?? 0.0) ?? ""
      ).accentColor(Color.yellow)
      SummaryMetricView(
          title: "Total Distance",
          value: Measurement(
              value: workoutManager.workout?.totalDistance?
                  .doubleValue(for: .meter()) ?? 0,
              unit: UnitLength.meters
          ).formatted(
              .measurement(
                  width: .abbreviated,
                  usage: .road
              )
          )
      ).accentColor(Color.green)
      SummaryMetricView(
          title: "Total Energy",
          value: Measurement(
              value: workoutManager.workout?.totalEnergyBurned?
                              .doubleValue(for: .kilocalorie()) ?? 0,
              unit: UnitEnergy.kilocalories
          ).formatted(
              .measurement(
                  width: .abbreviated,
                  usage: .workout,
                  numberFormat: .numeric(precision: .fractionLength(0))
              )
          )
      ).accentColor(Color.pink)
      SummaryMetricView(
          title: "Avg. Heart Rate",
          value: workoutManager.averageHeartRate
              .formatted(
                  .number.precision(.fractionLength(0))
              )
          + " bpm"
      ).accentColor(Color.red)
    • 51:45 - SessionPagingView - add isLuminanceReduced

      @Environment(\.isLuminanceReduced) var isLuminanceReduced
    • 51:57 - SessionPagingView - add tabViewStyle and onChangeOf based on isLuminanceReduced

      .tabViewStyle(
          PageTabViewStyle(indexDisplayMode: isLuminanceReduced ? .never : .automatic)
      )
      .onChange(of: isLuminanceReduced) { _ in
          displayMetricsView()
      }
  • 特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。

    クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。

Developer Footer

  • ビデオ
  • WWDC21
  • Apple Watch用ワークアウトAppの構築
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン