Files
workouts/Workouts Watch App/Views/Exercises/ExerciseProgressControlView.swift
2025-08-08 21:09:11 -04:00

186 lines
6.3 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ExerciseProgressControlView 2.swift
// Workouts
//
// Created by rzen on 7/23/25 at 9:15AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct ExerciseProgressControlView: View {
let log: WorkoutLog
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@State private var exerciseStates: [ExerciseState] = []
@State private var currentStateIndex: Int = 0
@State private var elapsedSeconds: Int = 0
@State private var timer: Timer? = nil
@State private var previousStateIndex: Int = 0
@State private var hapticCounter: Int = 0
var body: some View {
TabView(selection: $currentStateIndex) {
ForEach(Array(exerciseStates.enumerated()), id: \.element.id) { index, state in
if state.isIntro {
ExerciseIntroCard(log: log)
.tag(index)
} else if state.isSet {
ExerciseSetCard(set: state.setNumber ?? 0, elapsedSeconds: elapsedSeconds)
.tag(index)
} else if state.isRest {
ExerciseRestCard(elapsedSeconds: elapsedSeconds, afterSet: state.afterSet ?? 0)
.tag(index)
} else if state.isDone {
ExerciseDoneCard(elapsedSeconds: elapsedSeconds, onComplete: completeExercise, onOneMoreSet: addOneMoreSet)
.tag(index)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.gesture(
DragGesture()
.onEnded { value in
// Detect swipe left when on the Done card (last index)
if currentStateIndex == exerciseStates.count - 1 && value.translation.width < -50 {
// User swiped left from Done card - add one more set
addOneMoreSet()
}
}
)
.onChange(of: currentStateIndex) { oldValue, newValue in
if oldValue != newValue {
elapsedSeconds = 0
hapticCounter = 0 // Reset haptic pattern when changing phases
// Update the log's current state but don't auto-advance
log.currentStateIndex = currentStateIndex
log.elapsedSeconds = elapsedSeconds
try? modelContext.save()
// Update status based on current state
if currentStateIndex > 0 && currentStateIndex < exerciseStates.count - 1 {
log.status = .inProgress
}
}
}
.onAppear {
setupExerciseStates()
currentStateIndex = log.currentStateIndex ?? 0
startTimer()
}
.onChange(of: log.sets) { oldValue, newValue in
// Reconstruct exercise states if sets count changed
if oldValue != newValue {
setupExerciseStates()
}
}
.onDisappear {
stopTimer()
}
}
private func setupExerciseStates() {
var states: [ExerciseState] = []
states.append(.intro)
for i in 1...log.sets {
states.append(.set(number: i))
if i < log.sets {
states.append(.rest(afterSet: i))
}
}
states.append(.done)
exerciseStates = states
}
private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
elapsedSeconds += 1
// Check if we need to provide haptic feedback
if currentStateIndex >= 0 && currentStateIndex < exerciseStates.count {
let currentState = exerciseStates[currentStateIndex]
if currentState.isRest || currentState.isSet {
provideHapticFeedback()
} else if currentState.isDone && elapsedSeconds >= 10 {
// Auto-complete after 10 seconds on the DONE state
completeExercise()
}
}
}
}
private func stopTimer() {
timer?.invalidate()
timer = nil
}
private func provideHapticFeedback() {
// Provide haptic feedback every 15 seconds in a cycling pattern: 1 2 3 long
if elapsedSeconds % 15 == 0 && elapsedSeconds > 0 {
hapticCounter += 1
switch hapticCounter % 4 {
case 1:
// First 15 seconds: single tap
HapticFeedback.click()
case 2:
// Second 15 seconds: double tap
HapticFeedback.doubleTap()
case 3:
// Third 15 seconds: triple tap
HapticFeedback.tripleTap()
case 0:
// Fourth 15 seconds: long tap, then reset pattern
HapticFeedback.longTap()
default:
break
}
}
}
private func addOneMoreSet() {
// Increment total sets
log.sets += 1
// Reconstruct exercise states (will trigger onChange)
setupExerciseStates()
// Calculate the state index for the additional set
// States: intro(0) set1(1) rest1(2) ... setN(2N-1) done(2N)
// For the additional set, we want to go to setN which is at index 2N-1
let additionalSetStateIndex = (log.sets * 2) - 1
log.status = .inProgress
log.currentStateIndex = additionalSetStateIndex
log.elapsedSeconds = 0
elapsedSeconds = 0
hapticCounter = 0
// Update the current state index for the TabView
currentStateIndex = additionalSetStateIndex
try? modelContext.save()
}
private func completeExercise() {
// Update the workout log status to completed
log.status = .completed
// reset index in case we wish to re-run the exercise
log.currentStateIndex = 0
// Provide "tada" haptic feedback
HapticFeedback.tripleTap()
// Dismiss this view to return to WorkoutDetailView
dismiss()
}
}