ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Apple Watch用ワークアウトAppの構築
このCode-AlongではSwiftUIとHealthKitを使用してワークアウトAppを一から構築します。タイムラインを使用してAlways On(常にオン)ステートをサポートしワークアウトの指標を更新する方法について確認します。ワークアウトAppのベストデザインプラクティスに従って構築しましょう。
リソース
関連ビデオ
WWDC23
WWDC21
-
ダウンロード
♪ (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() }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。