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:
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// WorkoutSessionManager.swift
|
||||
// Workouts Watch App
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HealthKit
|
||||
import Observation
|
||||
|
||||
/// Runs an `HKWorkoutSession` for the duration of a watch workout.
|
||||
///
|
||||
/// When the phone launches us via `startWatchApp(toHandle:)`, watchOS hands the app
|
||||
/// delegate a workout configuration; starting a session with it is what grants the
|
||||
/// freshly-launched app foreground runtime (and keeps it alive while the wrist is
|
||||
/// down). We don't record samples — the session exists purely to host the UI.
|
||||
@Observable
|
||||
@MainActor
|
||||
final class WorkoutSessionManager: NSObject {
|
||||
private let healthStore = HKHealthStore()
|
||||
private var session: HKWorkoutSession?
|
||||
|
||||
private(set) var isRunning = false
|
||||
|
||||
/// Prompt for workout-sharing authorization. Called once at launch so a later
|
||||
/// phone-initiated launch can start a session without first hitting a permission
|
||||
/// wall.
|
||||
func requestAuthorization() {
|
||||
guard HKHealthStore.isHealthDataAvailable() else { return }
|
||||
healthStore.requestAuthorization(toShare: [.workoutType()], read: []) { _, _ in }
|
||||
}
|
||||
|
||||
/// Start the session for a phone-launched workout. Idempotent — a second launch
|
||||
/// while one is already running is ignored.
|
||||
func start(with configuration: HKWorkoutConfiguration) {
|
||||
guard HKHealthStore.isHealthDataAvailable(), session == nil else { return }
|
||||
do {
|
||||
let session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
|
||||
session.delegate = self
|
||||
self.session = session
|
||||
session.startActivity(with: Date())
|
||||
isRunning = true
|
||||
} catch {
|
||||
session = nil
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
/// End the session — called when the workout finishes (no active workout remains).
|
||||
func end() {
|
||||
session?.end()
|
||||
clear()
|
||||
}
|
||||
|
||||
private func clear() {
|
||||
session = nil
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HKWorkoutSessionDelegate
|
||||
|
||||
extension WorkoutSessionManager: HKWorkoutSessionDelegate {
|
||||
nonisolated func workoutSession(
|
||||
_ workoutSession: HKWorkoutSession,
|
||||
didChangeTo toState: HKWorkoutSessionState,
|
||||
from fromState: HKWorkoutSessionState,
|
||||
date: Date
|
||||
) {
|
||||
guard toState == .ended || toState == .stopped else { return }
|
||||
Task { @MainActor in self.clear() }
|
||||
}
|
||||
|
||||
nonisolated func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
|
||||
Task { @MainActor in self.clear() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user