ハイライト
Apple は SwiftUI と HealthKit で Apple Watch 運動 App をゼロから構築するデモを行いました。リアルタイムの心拍、エネルギー、距離を収集し、運動記録を保存し、TimelineView で Always On に適応します。
主要内容
Apple Watch 運動 App を作る上で、難しいのはボタンの数ではありません。
本当の難しさは運動開始後に始まります。ユーザーが走ったり漕いだりすると、手首は上下し、画面は Always On の低頻度更新に入ります。App は経過時間、エネルギー、心拍、距離を表示し続け、終了後に HealthKit に記録を保存する必要があります。
以前はこのロジックを SwiftUI ビューに散らばせると、UI、権限、セッション状態、データ収集が混ざります。一時停止ボタンはセッション状態を変え、サマリーページは保存完了を待ち、リアルタイム指標は画面の更新頻度に合わせる必要がありました。
この Code-Along では Apple が完全なパスに分解します。SwiftUI の 3 画面構造、WorkoutManager による HealthKit セッション管理、Always On 下で指標ページを適切に更新する TimelineView です。
ワンタップで運動を開始
ユーザーが運動 App を開いたとき、最初に複雑な設定を見せるべきではありません。起点は StartView で、3 種類の運動——自転車、ランニング、ウォーキングです。各項目は NavigationLink で直接運動セッションに入れます。
タップ後、運動画面は watchOS 共通の 3 ページ構成になります。左にコントロール、中央に指標、右に Now Playing。デフォルトは指標ページ——運動開始後、ユーザーが最も気にするのは時間、エネルギー、心拍、距離です。
1 つのオブジェクトで運動ライフサイクルを管理
HealthKit の運動セッションには明確なフローがあります。権限要求、設定作成、HKWorkoutSession の開始、HKLiveWorkoutBuilder によるデータ収集、完了時に HKWorkout として保存です。
このセッションではその状態を WorkoutManager に置きます。SwiftUI ビューは @EnvironmentObject で読み取ります。ボタンは一時停止や終了だけをトリガーし、指標ページは公開されたデータを表示するだけです。
Always On が更新方式を変える
Apple Watch の Always On は更新頻度を下げます。運動 App は高頻度で百分秒を表示し続けられません——不適切であり、Always On の情報ニーズにも合いません。
解決策は TimelineView です。TimelineScheduleMode で更新間隔を決め、context.cadence で百分秒表示を制御します。画面がアクティブなときは細かい時間、低頻度状態では分と秒だけを表示します。
詳細
運動タイプのリスト
(03:25) Apple は HealthKit の HKWorkoutActivityType で運動タイプをモデル化します。SwiftUI の List に直接使うため、列挙型に Identifiable と表示名を追加します。
import HealthKit
var workoutTypes: [HKWorkoutActivityType] = [.cycling, .running, .walking]
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 ""
}
}
}
キーポイント:
import HealthKitでHKWorkoutActivityTypeを導入する。workoutTypesは表示する 3 種類の運動を保持する。Identifiableにより列挙値をListのデータソースにできる。idはrawValueを使い、HealthKit 列挙の生の値に対応する。nameは HealthKit タイプを時計 UI の短いラベルに変換する。
(04:22) リストは carousel スタイルを使います。watchOS で縦スクロールリストに適した視覚効果です。
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")
キーポイント:
List(workoutTypes)は各運動タイプごとに 1 行を生成する。NavigationLinkは各行から次ページへの遷移を提供する。paddingは運動前の素早い操作向けにタップ領域を広げる。.listStyle(.carousel)は watchOS のカルーセルリスト効果を使う。.navigationBarTitle("Workouts")で開始ページのタイトルを設定する。
運動中の 3 ページ構成
(06:55) 運動開始後、ユーザーはコントロール、リアルタイム指標、メディアが必要です。TabView で横方向ページングし、デフォルトを指標ページにします。
@State private var selection: Tab = .metrics
enum Tab {
case controls, metrics, nowPlaying
}
TabView(selection: $selection) {
Text("Controls").tag(Tab.controls)
Text("Metrics").tag(Tab.metrics)
Text("Now Playing").tag(Tab.nowPlaying)
}
キーポイント:
selectionは現在のページを保持する。- デフォルトは
.metrics——運動開始後はまずデータを表示する。 Tab列挙は 3 ページを列挙する。TabView(selection:)は現在のページ状態をバインドする。tagは各子ビューを列挙値に対応づける。
(16:09) プレースホルダーテキストはその後、実際のビューに置き換えられます。
ControlsView().tag(Tab.controls)
MetricsView().tag(Tab.metrics)
NowPlayingView().tag(Tab.nowPlaying)
キーポイント:
ControlsViewは終了・一時停止・再開ボタンを置く。MetricsViewは経過時間、エネルギー、心拍、距離を表示する。NowPlayingViewは運動中のメディアコントロールを提供する。- 3 ページすべてが同じ
Tabタグを再利用する。
経過時間のフォーマット
(11:42) 経過時間は普通の数字では表示できません。ElapsedTimeView と ElapsedTimeFormatter を作り、DateComponentsFormatter で分と秒を出力し、必要に応じて百分秒を追加します。
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
}
}
キーポイント:
ElapsedTimeViewはTimeIntervalを受け取る。showSubsecondsは百分秒表示を制御する。Text(NSNumber(value:), formatter:)で SwiftUI にカスタム formatter を使わせる。DateComponentsFormatterは分と秒を担当する。allowedUnits = [.minute, .second]で出力単位を限定する。zeroFormattingBehavior = .padで03:15のようなゼロ埋め形式を保つ。truncatingRemainder(dividingBy: 1)で小数部分を取得する。Locale.current.decimalSeparatorは現在のロケールの小数点を使う。
HealthKit 権限のリクエスト
(29:35) 運動 App は workout を書き込み、心拍、エネルギー、距離、アクティビティリングを読み取ります。HealthKit 権限は WorkoutManager で一括リクエストします。
// 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.
}
}
キーポイント:
typesToShareは書き込む運動記録を宣言する。HKQuantityType.workoutType()は workout タイプを表す。typesToReadは読み取る HealthKit データを宣言する。.heartRateは現在と平均心拍に使う。.activeEnergyBurnedは活動エネルギーに使う。.distanceWalkingRunningと.distanceCyclingは歩行・ランニング距離とサイクリング距離に対応する。HKObjectType.activitySummaryType()はサマリーのアクティビティリングに使う。requestAuthorizationはシステム権限リクエストを開始する。
(30:20) 開始ページが表示されたときに権限をリクエストします。
.onAppear {
workoutManager.requestAuthorization()
}
キーポイント:
.onAppearは開始ページ表示時に実行される。workoutManagerは SwiftUI 環境に注入される共有オブジェクトである。- 権限フローはユーザーが運動を始める前に行われる。
運動セッションの開始
(27:42) リアルタイムの HealthKit 運動は HKWorkoutSession と HKLiveWorkoutBuilder に依存します。前者がセッション状態、後者がリアルタイムデータ収集と最終 workout 生成を担当します。
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.
}
}
キーポイント:
HKWorkoutConfigurationは運動タイプと位置タイプを保持する。activityTypeは開始ページでのユーザー選択から来る。locationType = .outdoorは屋外運動に設定する。HKWorkoutSessionは HealthKit 運動セッションを作成する。associatedWorkoutBuilder()はセッションに紐づく builder を取得する。HKLiveWorkoutDataSourceは builder がリアルタイム HealthKit データを取得できるようにする。startDateはセッションとデータ収集の共通起点である。startActivity(with:)で運動セッションを開始する。beginCollection(withStart:)でリアルタイムサンプルの収集を開始する。
(29:06) 運動タイプを選ぶと自動的にセッションが開始されます。
var selectedWorkout: HKWorkoutActivityType? {
didSet {
guard let selectedWorkout = selectedWorkout else { return }
startWorkout(workoutType: selectedWorkout)
}
}
キーポイント:
selectedWorkoutはユーザーが選んだ運動を記録する。didSetは選択が変わった後に実行される。guardは nil を除外する。startWorkout(workoutType:)はナビゲーション選択と HealthKit セッションを接続する。
一時停止、再開、終了
(33:29) 運動コントロールは WorkoutManager 経由で公開されます。UI ボタンは HealthKit セッションに直接触れず、これらのメソッドだけを呼びます。
// 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()
}
キーポイント:
@Published var runningは実行状態を SwiftUI に公開する。pause()は HealthKit セッションを一時停止する。resume()は HealthKit セッションを再開する。togglePause()は現在の状態に応じて動作を切り替える。endWorkout()はセッションを終了する。
(34:11) セッション状態の変化は HKWorkoutSessionDelegate で返されます。終了時はまずデータ収集を止め、その後 workout 保存を完了します。
// 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) {
}
}
キーポイント:
HKWorkoutSessionDelegateはセッション状態変化を受け取る。didChangeToは新状態、旧状態、変化時刻を提供する。DispatchQueue.main.asyncでrunning更新をメインスレッドに戻す。toState == .runningは一時停止ボタン状態の更新に使う。toState == .endedはセッション終了を意味する。endCollection(withEnd:)でデータ収集を停止する。finishWorkoutは保存しHKWorkoutを生成する。
リアルタイム指標の収集
(40:25) リアルタイム指標は @Published で SwiftUI に公開されます。
// MARK: - Workout Metrics
@Published var averageHeartRate: Double = 0
@Published var heartRate: Double = 0
@Published var activeEnergy: Double = 0
@Published var distance: Double = 0
キーポイント:
averageHeartRateは平均心拍を保持する。heartRateは直近の心拍を保持する。activeEnergyは累積活動エネルギーを保持する。distanceは累積距離を保持する。@Publishedは指標変化時にバインドされたビューを自動更新する。
(41:05) HKLiveWorkoutBuilderDelegate はどのサンプルタイプに新データがあるかを通知します。
// 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)
}
}
}
キーポイント:
HKLiveWorkoutBuilderDelegateは builder の収集イベントを受け取る。didCollectDataOfは今回の更新に関わるデータタイプを提供する。collectedTypesには心拍、エネルギー、距離が含まれる場合がある。guard let quantityTypeは数量タイプのみを処理する。statistics(for:)は builder から統計を取得する。updateForStatisticsは HealthKit 統計を UI データに変換する。
(42:01) 指標ごとに異なる単位と統計方法を使います。
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
}
}
}
キーポイント:
- 空の
statisticsでは即座に return する。 - UI 関連状態はメインスレッドで更新する。
- 心拍単位は
count/minuteである。 mostRecentQuantity()は現在の心拍を取得する。averageQuantity()は平均心拍を取得する。- 活動エネルギーはキロカロリー単位を使う。
sumQuantity()は累積エネルギーを取得する。- 歩行・ランニング距離とサイクリング距離はメートルに変換する。
- 未処理タイプは
defaultに入る。
リアルタイム指標の表示
(43:35) 指標ページは WorkoutManager から値を読み取り、Measurement.formatted で地域と運動シーンに適したテキストを生成します。
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
)
)
)
}
キーポイント:
ElapsedTimeViewは builder の経過時間を表示する。?? 0は builder 未作成時のデフォルトを与える。- エネルギーは
UnitEnergy.kilocaloriesを使う。 .measurement(width: .abbreviated, usage: .workout)は運動シーン向けの短い単位を出力する。numberFormatでエネルギーの小数を除去する。- 心拍は数値フォーマットに
bpmを追加する。 - 距離は
UnitLength.metersを入力単位にする。 usage: .roadで距離をロケールに応じて自然に表示する。
Always On でのタイムライン更新
(45:51) Always On では運動指標の更新頻度を下げる必要があります。TimelineSchedule をカスタマイズします。
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
)
}
}
キーポイント:
MetricsTimelineScheduleはTimelineScheduleに準拠する。startDateは運動開始時刻を記録する。entries(from:mode:)はシステムモードに応じて更新時刻を生成する。mode == .lowFrequencyは低頻度更新を意味する。- 低頻度では 1 秒ごとに更新する。
- アクティブ状態では
1.0 / 30.0秒ごとに更新する。 PeriodicTimelineScheduleは周期的な entries を作る。
(46:38) 指標ビューを TimelineView に入れ、context.cadence に基づいて百分秒を非表示にします。
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()
}
キーポイント:
TimelineViewはカスタムタイムラインでコンテンツを再計算する。fromは builder の開始時刻を使う。context.cadence == .liveはリアルタイム更新状態を意味する。- リアルタイム状態では百分秒を表示する。
- 低頻度状態では百分秒を隠す。
- rounded title フォントは時計での素早い読み取りに適する。
monospacedDigit()は数字のジャンプによる視覚的揺れを防ぐ。.ignoresSafeArea(edges: .bottom)で指標領域が下部スペースを使う。.scenePadding()は watchOS のシーンパディングに揃える。
(51:45) ページレベルでは isLuminanceReduced を読み取り、Always On 低輝度でページインジケーターを隠し、自動的に指標ページに戻ります。
@Environment(\.isLuminanceReduced) var isLuminanceReduced
.tabViewStyle(
PageTabViewStyle(indexDisplayMode: isLuminanceReduced ? .never : .automatic)
)
.onChange(of: isLuminanceReduced) { _ in
displayMetricsView()
}
キーポイント:
isLuminanceReducedは SwiftUI 環境から来る。- 低輝度は通常 Always On 表示に対応する。
PageTabViewStyleはページインジケーターを制御する。- 低輝度では
.neverでインジケーターを隠す。 - 通常状態では
.automaticを使う。 onChangeは低輝度状態の変化を監視する。- 状態変化時に
displayMetricsView()を呼び指標ページに戻る。
運動サマリーとアクティビティリング
(21:00) 終了後、サマリーで当日のアクティビティリングを表示できます。SwiftUI は WKInterfaceObjectRepresentable で WatchKit の WKInterfaceActivityRing をラップします。
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) {
}
}
キーポイント:
WKInterfaceObjectRepresentableは WatchKit オブジェクトを SwiftUI にブリッジする。healthStoreは HealthKit クエリを実行する。WKInterfaceActivityRingはアクティビティリングコントロールである。dateComponentsは当日の日付を取得する。predicateForActivitySummaryは当日のアクティビティサマリークエリを作る。HKActivitySummaryQueryはアクティビティサマリーを読み取る。setActivitySummaryはクエリ結果をリングコントロールに設定する。DispatchQueue.main.asyncで UI 更新をメインスレッドで行う。
(50:43) 最終サマリーは保存された HKWorkout から総時間、総距離、総エネルギーを読み取ります。
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)
キーポイント:
workoutManager.workoutは保存完了後のHKWorkoutである。durationは総時間を提供する。totalDistanceは総距離を提供する。doubleValue(for: .meter())でメートルに変換する。totalEnergyBurnedは総エネルギーを提供する。doubleValue(for: .kilocalorie())でキロカロリーに変換する。- 平均心拍はリアルタイム収集時に保存した
averageHeartRateから来る。 - 各
SummaryMetricViewは異なるaccentColorで指標を区別する。
重要ポイント
-
何をするか: 屋外ウォーキング記録器を作り、開始・一時停止・終了と 4 つのコア指標だけを残す。 なぜ価値があるか: このセッションは既に
HKWorkoutSession、HKLiveWorkoutBuilder、指標ページ構造を提供している。 どう始めるか:workoutTypesを.walkingに絞り、startWorkout(workoutType:)でセッション開始、distanceWalkingRunningとheartRateで UI を更新。 -
何をするか: サイクリング向けのミニマル心拍画面を作る。 なぜ価値があるか:
TimelineViewは Always On でも読みやすい情報を保ち、monospacedDigit()は運動中のちらっと見に適している。 どう始めるか:MetricsTimelineScheduleを再利用し、heartRate、averageHeartRate、経過時間だけ表示。context.cadence == .liveで時間精度を制御。 -
何をするか: 運動後の振り返りサマリーページを作る。 なぜ価値があるか:
HKWorkoutには既に総時間、総距離、総エネルギーがあり、アクティビティリングはHKActivitySummaryQueryで取得できる。 どう始めるか:finishWorkout後にworkoutを保存し、SummaryMetricViewでduration、totalDistance、totalEnergyBurnedを表示し、ActivityRingsViewを埋め込む。 -
何をするか: Apple Watch メディアコントロール付きランニング App を作る。 なぜ価値があるか: 運動中は曲の切り替えが必要で、3 ページ構成は既に右側に
NowPlayingViewを置いている。 どう始めるか:SessionPagingViewにNowPlayingView().tag(Tab.nowPlaying)を残し、PageTabViewStyleとisLuminanceReducedで Always On のページ表示を制御。 -
何をするか: 走走交替トレーニングなど、特定トレーニング向けの watchOS ツールを作る。 なぜ価値があるか:
running、pause()、resume()、togglePause()がセッション制御を抽象化し、ビューはトレーニングフローに集中できる。 どう始めるか:WorkoutManager外にトレーニングフェーズ状態を追加し、フェーズ切り替えで SwiftUI テキストを更新。HealthKit はsession?.pause()とsession?.resume()を使う。
関連セッション
- What’s new in watchOS 8 — watchOS 8 のシステム能力。Always On と文字盤のコンテキスト
- What’s new in SwiftUI — SwiftUI 2021 の更新。watchOS UI 構築の基礎
- Connect Bluetooth devices to Apple Watch — 運動センサー向けに外部 Bluetooth デバイスを統合
- Create accessible experiences for watchOS — Apple Watch 上の運動 App のアクセシビリティを改善
コメント
GitHub Issues · utterances