318 lines
9.6 KiB
Swift
318 lines
9.6 KiB
Swift
import SwiftUI
|
||
import SwiftData
|
||
import WatchKit
|
||
|
||
// Enum to track the current phase of the exercise
|
||
enum ExercisePhase {
|
||
case notStarted
|
||
case exercising(setNumber: Int)
|
||
case resting(setNumber: Int, elapsedSeconds: Int)
|
||
case completed
|
||
}
|
||
|
||
struct ExerciseProgressView: View {
|
||
@Environment(\.modelContext) private var modelContext
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
let log: WorkoutLog
|
||
|
||
@State private var phase: ExercisePhase = .notStarted
|
||
@State private var hapticSeconds: Int = 0
|
||
@State private var restSeconds: Int = 0
|
||
@State private var hapticTimer: Timer? = nil
|
||
@State private var restTimer: Timer? = nil
|
||
|
||
var body: some View {
|
||
ScrollView {
|
||
VStack(spacing: 15) {
|
||
exerciseHeader
|
||
|
||
switch phase {
|
||
case .notStarted:
|
||
startPhaseView
|
||
case .exercising(let setNumber):
|
||
exercisingPhaseView(setNumber: setNumber)
|
||
case .resting(let setNumber, let elapsedSeconds):
|
||
restingPhaseView(setNumber: setNumber, elapsedSeconds: elapsedSeconds)
|
||
case .completed:
|
||
completedPhaseView
|
||
}
|
||
}
|
||
.padding()
|
||
}
|
||
.navigationTitle(log.exerciseName)
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.onDisappear {
|
||
stopTimers()
|
||
}
|
||
.gesture(
|
||
DragGesture(minimumDistance: 50)
|
||
.onEnded { gesture in
|
||
if gesture.translation.width < 0 {
|
||
// Swipe left - progress to next phase
|
||
handleSwipeLeft()
|
||
} else if gesture.translation.height < 0 && gesture.translation.height < -50 {
|
||
// Swipe up - cancel current set
|
||
handleSwipeUp()
|
||
}
|
||
}
|
||
)
|
||
}
|
||
|
||
// MARK: - View Components
|
||
|
||
private var exerciseHeader: some View {
|
||
VStack(alignment: .leading, spacing: 5) {
|
||
Text(log.exerciseName)
|
||
.font(.headline)
|
||
.foregroundColor(.primary)
|
||
|
||
Text("\(log.sets) sets × \(log.reps) reps")
|
||
.font(.subheadline)
|
||
.foregroundColor(.secondary)
|
||
|
||
Text("\(log.weight) lbs")
|
||
.font(.subheadline)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.bottom, 5)
|
||
}
|
||
|
||
private var startPhaseView: some View {
|
||
VStack(spacing: 20) {
|
||
Text("Ready to start?")
|
||
.font(.headline)
|
||
|
||
Button(action: startFirstSet) {
|
||
Text("Start First Set")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.tint(.blue)
|
||
}
|
||
}
|
||
|
||
private func exercisingPhaseView(setNumber: Int) -> some View {
|
||
VStack(spacing: 20) {
|
||
Text("Set \(setNumber) of \(log.sets)")
|
||
.font(.headline)
|
||
|
||
Text("Exercising...")
|
||
.foregroundColor(.secondary)
|
||
|
||
HStack(spacing: 20) {
|
||
Button(action: completeSet) {
|
||
Text("Complete")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.tint(.green)
|
||
|
||
Button(action: cancelSet) {
|
||
Text("Cancel")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.bordered)
|
||
.tint(.red)
|
||
}
|
||
|
||
Text("Or swipe left to complete")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
|
||
Text("Swipe up to cancel")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
}
|
||
|
||
private func restingPhaseView(setNumber: Int, elapsedSeconds: Int) -> some View {
|
||
VStack(spacing: 20) {
|
||
Text("Rest after Set \(setNumber)")
|
||
.font(.headline)
|
||
|
||
Text("Rest time: \(formatSeconds(elapsedSeconds))")
|
||
.foregroundColor(.secondary)
|
||
|
||
if setNumber < (log.sets) {
|
||
Button(action: { startNextSet(after: setNumber) }) {
|
||
Text("Start Set \(setNumber + 1)")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.tint(.blue)
|
||
|
||
Text("Or swipe left to start next set")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
} else {
|
||
Button(action: completeExercise) {
|
||
Text("Complete Exercise")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.tint(.green)
|
||
|
||
Text("Or swipe left to complete")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
}
|
||
}
|
||
|
||
private var completedPhaseView: some View {
|
||
VStack(spacing: 20) {
|
||
Text("Exercise Completed!")
|
||
.font(.headline)
|
||
.foregroundColor(.green)
|
||
|
||
Image(systemName: "checkmark.circle.fill")
|
||
.font(.system(size: 50))
|
||
.foregroundColor(.green)
|
||
|
||
Button(action: { dismiss() }) {
|
||
Text("Return to Workout")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.tint(.blue)
|
||
}
|
||
}
|
||
|
||
// MARK: - Action Handlers
|
||
|
||
private func handleSwipeLeft() {
|
||
switch phase {
|
||
case .notStarted:
|
||
startFirstSet()
|
||
case .exercising:
|
||
completeSet()
|
||
case .resting(let setNumber, _):
|
||
if setNumber < (log.sets) {
|
||
startNextSet(after: setNumber)
|
||
} else {
|
||
completeExercise()
|
||
}
|
||
case .completed:
|
||
dismiss()
|
||
}
|
||
}
|
||
|
||
private func handleSwipeUp() {
|
||
if case .exercising = phase {
|
||
cancelSet()
|
||
}
|
||
}
|
||
|
||
private func startFirstSet() {
|
||
phase = .exercising(setNumber: 1)
|
||
startHapticTimer()
|
||
}
|
||
|
||
private func startNextSet(after completedSetNumber: Int) {
|
||
stopTimers()
|
||
let nextSetNumber = completedSetNumber + 1
|
||
phase = .exercising(setNumber: nextSetNumber)
|
||
startHapticTimer()
|
||
}
|
||
|
||
private func completeSet() {
|
||
stopTimers()
|
||
|
||
if case .exercising(let setNumber) = phase {
|
||
// Start rest timer
|
||
phase = .resting(setNumber: setNumber, elapsedSeconds: 0)
|
||
startRestTimer()
|
||
startHapticTimer()
|
||
|
||
// Play completion haptic
|
||
HapticFeedback.success()
|
||
}
|
||
}
|
||
|
||
private func cancelSet() {
|
||
// Just go back to the previous state
|
||
stopTimers()
|
||
phase = .notStarted
|
||
}
|
||
|
||
private func completeExercise() {
|
||
stopTimers()
|
||
|
||
// Update workout log
|
||
log.completed = true
|
||
log.status = .completed
|
||
try? modelContext.save()
|
||
|
||
// Show completion screen
|
||
phase = .completed
|
||
|
||
// Play completion haptic
|
||
HapticFeedback.success()
|
||
}
|
||
|
||
// MARK: - Timer Management
|
||
|
||
private func startHapticTimer() {
|
||
hapticSeconds = 0
|
||
hapticTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||
hapticSeconds += 1
|
||
|
||
// Provide haptic feedback based on time intervals
|
||
if hapticSeconds % 60 == 0 {
|
||
// Triple tap every 60 seconds
|
||
HapticFeedback.tripleTap()
|
||
} else if hapticSeconds % 30 == 0 {
|
||
// Double tap every 30 seconds
|
||
HapticFeedback.doubleTap()
|
||
} else if hapticSeconds % 10 == 0 {
|
||
// Light tap every 10 seconds
|
||
HapticFeedback.click()
|
||
}
|
||
}
|
||
}
|
||
|
||
private func startRestTimer() {
|
||
restSeconds = 0
|
||
restTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||
restSeconds += 1
|
||
|
||
if case .resting(let setNumber, _) = phase {
|
||
phase = .resting(setNumber: setNumber, elapsedSeconds: restSeconds)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func stopTimers() {
|
||
hapticTimer?.invalidate()
|
||
hapticTimer = nil
|
||
|
||
restTimer?.invalidate()
|
||
restTimer = nil
|
||
}
|
||
|
||
// MARK: - Helper Functions
|
||
|
||
private func formatSeconds(_ seconds: Int) -> String {
|
||
let minutes = seconds / 60
|
||
let remainingSeconds = seconds % 60
|
||
return String(format: "%d:%02d", minutes, remainingSeconds)
|
||
}
|
||
}
|
||
|
||
//#Preview {
|
||
// let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||
// let container = try! ModelContainer(for: SchemaV1.models, configurations: config)
|
||
//
|
||
// // Create sample data
|
||
// let exercise = Exercise(name: "Bench Press", sets: 3, reps: 8, weight: 60.0)
|
||
// let workout = Workout(name: "Chest Day", date: Date())
|
||
// let log = WorkoutLog(exercise: exercise, workout: workout)
|
||
//
|
||
// NavigationStack {
|
||
// ExerciseProgressView(log: log)
|
||
// .modelContainer(container)
|
||
// }
|
||
//}
|