initial pre-viable version of watch app
This commit is contained in:
317
Worksouts Watch App/Views/ExerciseProgressView.swift
Normal file
317
Worksouts Watch App/Views/ExerciseProgressView.swift
Normal file
@ -0,0 +1,317 @@
|
||||
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)
|
||||
// }
|
||||
//}
|
Reference in New Issue
Block a user