d5915a9552
- 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
79 lines
2.6 KiB
Swift
79 lines
2.6 KiB
Swift
//
|
|
// 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() }
|
|
}
|
|
}
|