186 lines
6.3 KiB
Swift
186 lines
6.3 KiB
Swift
//
|
||
// ExerciseProgressControlView 2.swift
|
||
// Workouts
|
||
//
|
||
// Created by rzen on 7/23/25 at 9:15 AM.
|
||
//
|
||
// 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()
|
||
}
|
||
}
|