Files
workouts/Worksouts Watch App/Views/ExerciseProgressControlView.swift

269 lines
7.5 KiB
Swift

//
// ExerciseProgressControlView.swift
// Workouts
//
// Created by rzen on 7/20/25 at 7:19 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
enum ExerciseState: Identifiable {
case set(number: Int)
case rest(afterSet: Int)
case done
var id: String {
switch self {
case .set(let number):
return "set_\(number)"
case .rest(let afterSet):
return "rest_\(afterSet)"
case .done:
return "done"
}
}
var isRest: Bool {
if case .rest = self {
return true
}
return false
}
var isSet: Bool {
if case .set = self {
return true
}
return false
}
var isDone: Bool {
if case .done = self {
return true
}
return false
}
}
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
var body: some View {
TabView(selection: $currentStateIndex) {
ForEach(Array(exerciseStates.enumerated()), id: \.element.id) { index, state in
ExerciseStateView(
state: state,
elapsedSeconds: elapsedSeconds,
onComplete: {
moveToNextState()
}
)
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.onChange(of: currentStateIndex) { oldValue, newValue in
if oldValue != newValue {
// Reset timer when user swipes to a new state
elapsedSeconds = 0
}
}
.onAppear {
setupExerciseStates()
startTimer()
}
.onDisappear {
stopTimer()
}
}
private func setupExerciseStates() {
var states: [ExerciseState] = []
// Create states for each set and rest period
for setNumber in 1...log.sets {
states.append(.set(number: setNumber))
// Add rest period after each set except the last one
if setNumber < log.sets {
states.append(.rest(afterSet: setNumber))
}
}
// Add done state at the end
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 during rest periods
if let currentState = exerciseStates[safe: currentStateIndex] {
if currentState.isRest {
provideRestHapticFeedback()
} 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 moveToNextState() {
if currentStateIndex < exerciseStates.count - 1 {
withAnimation {
currentStateIndex += 1
elapsedSeconds = 0
}
} else {
// We've reached the end (DONE state)
completeExercise()
}
}
private func provideRestHapticFeedback() {
// Provide haptic feedback based on elapsed time
if elapsedSeconds % 60 == 0 && elapsedSeconds > 0 {
// Triple tap every 60 seconds
HapticFeedback.tripleTap()
} else if elapsedSeconds % 30 == 0 && elapsedSeconds > 0 {
// Double tap every 30 seconds
HapticFeedback.doubleTap()
} else if elapsedSeconds % 10 == 0 && elapsedSeconds > 0 {
// Single tap every 10 seconds
HapticFeedback.success()
}
}
private func completeExercise() {
// Update the workout log status to completed
log.status = .completed
// Provide "tada" haptic feedback
HapticFeedback.tripleTap()
// Dismiss this view to return to WorkoutDetailView
dismiss()
}
}
struct ExerciseStateView: View {
let state: ExerciseState
let elapsedSeconds: Int
let onComplete: () -> Void
var body: some View {
VStack(spacing: 20) {
// Title based on state
Text(stateTitle)
.font(.title3)
.fontWeight(.bold)
// Timer display
Text(timeFormatted)
.font(.system(size: 48, weight: .semibold, design: .monospaced))
.foregroundStyle(state.isRest ? .orange : .accentColor)
// Only show Done button and countdown for the final state
if state.isDone {
// Countdown message
if elapsedSeconds < 10 {
Text("Completing automatically in \(10 - elapsedSeconds) seconds")
.font(.caption)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
} else {
Text("Auto-completing...")
.font(.caption)
.foregroundStyle(.secondary)
}
// Done button
Button(action: onComplete) {
Text("Done")
.font(.headline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.green)
.padding(.horizontal)
}
}
.padding()
}
private var stateTitle: String {
switch state {
case .set(let number):
return "Set \(number) in progress"
case .rest:
return "Resting"
case .done:
return "Exercise Complete"
}
}
private var buttonTitle: String {
switch state {
case .set:
return "Complete Set"
case .rest:
return "Start Next Set"
case .done:
return "DONE"
}
}
private var buttonColor: Color {
switch state {
case .set:
return .accentColor
case .rest:
return .orange
case .done:
return .green
}
}
private var timeFormatted: String {
let minutes = elapsedSeconds / 60
let seconds = elapsedSeconds % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
// Extension to safely access array elements
extension Array {
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
//#Preview {
// let container = AppContainer.preview
// let workout = Workout(start: Date(), end: nil, split: nil)
// let log = WorkoutLog(workout: workout, exerciseName: "Bench Press", date: Date(), sets: 3, reps: 10, weight: 135)
//
// ExerciseProgressControlView(log: log)
// .modelContainer(container)
//}