Add HIIT watch runner, rest-time setting, and HealthKit watch auto-launch

- Redesign the watch app into an active-workout runner: a root gate shows the
  in-progress workout's exercises or prompts to start one on iPhone, and each
  exercise runs as a horizontally-paged HIIT cycle (count-up work, count-down
  rest with final-three-second haptics + auto-advance, One More / Done on the
  last set). Replaces the old history list.
- Add a configurable rest-between-sets duration in iPhone Settings (default 45s),
  synced to the watch over WatchConnectivity.
- Launch the watch app into the session when a workout starts on the phone via
  HealthKit (startWatchApp); the watch runs an HKWorkoutSession for foreground
  runtime and ends it when the workout finishes. Adds the HealthKit entitlement +
  Health usage strings on both targets and WKBackgroundModes on the watch.

Claude-Session: https://claude.ai/code/session_018gg69MaUetDNzWzBXisfMV
This commit is contained in:
2026-06-19 16:16:44 -04:00
parent 3ed7b9272c
commit d5915a9552
22 changed files with 493 additions and 283 deletions
+40
View File
@@ -0,0 +1,40 @@
//
// WorkoutLauncher.swift
// Workouts
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import Foundation
import HealthKit
/// Launches the companion Watch app into a workout when a session starts on the phone.
///
/// An iPhone app can't foreground its Watch app on its own the only sanctioned path
/// is HealthKit's `startWatchApp(toHandle:)`, which hands watchOS a workout
/// configuration and brings the Watch app up to run a matching `HKWorkoutSession`.
/// Calling it requires authorization to *share* workouts first.
@MainActor
final class WorkoutLauncher {
private let healthStore = HKHealthStore()
/// Strength/HIIT configuration handed to watchOS; the watch starts a session with
/// the same shape (see `WorkoutSessionManager`).
static func makeConfiguration() -> HKWorkoutConfiguration {
let configuration = HKWorkoutConfiguration()
configuration.activityType = .traditionalStrengthTraining
configuration.locationType = .indoor
return configuration
}
/// Ask watchOS to launch the Watch app into the workout. Best-effort: no-ops where
/// HealthKit is unavailable (e.g. iPad without it) and silently tolerates a missing
/// or unreachable paired watch.
func launchWatchWorkout() {
guard HKHealthStore.isHealthDataAvailable() else { return }
Task {
try? await healthStore.requestAuthorization(toShare: [.workoutType()], read: [])
try? await healthStore.startWatchApp(toHandle: Self.makeConfiguration())
}
}
}