From c722d59aff08d37c92f8ec015cd0a064352f6a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Mon, 28 Oct 2024 12:00:56 +0100 Subject: [PATCH] add SimpleStopWatch to ActiveWorkoutSession --- WorkoutsPlus/Components/SimpleStopWatch.swift | 66 +++++++++++++++++++ WorkoutsPlus/Components/TimerView.swift | 39 ----------- WorkoutsPlus/Configuration/Defaults.swift | 3 +- WorkoutsPlus/Configuration/Protocols.swift | 5 ++ WorkoutsPlus/ContentView.swift | 10 +-- WorkoutsPlus/Features/Exercise/Exercise.swift | 10 ++- .../Features/Exercise/ExerciseEditor.swift | 11 +++- .../Features/Exercise/ExerciseUnit.swift | 28 ++++++++ WorkoutsPlus/Features/Food/Unit.swift | 27 -------- .../Features/Workout/WorkoutDetail.swift | 21 ++---- .../Features/Workout/WorkoutItem.swift | 3 +- .../Features/Workout/WorkoutLibrary.swift | 14 ++-- .../WorkoutSession/ActiveWorkoutSession.swift | 64 +++++++++--------- .../ActiveWorkoutSessionControls.swift | 8 +-- .../ActiveWorkoutSessionListItem.swift | 22 ++++++- .../WorkoutSession/WorkoutSession.swift | 26 ++++---- .../WorkoutSession/WorkoutSessionItem.swift | 8 ++- WorkoutsPlus/WorkoutsPlusApp.swift | 1 + 18 files changed, 213 insertions(+), 153 deletions(-) create mode 100644 WorkoutsPlus/Components/SimpleStopWatch.swift delete mode 100644 WorkoutsPlus/Components/TimerView.swift create mode 100644 WorkoutsPlus/Features/Exercise/ExerciseUnit.swift delete mode 100644 WorkoutsPlus/Features/Food/Unit.swift diff --git a/WorkoutsPlus/Components/SimpleStopWatch.swift b/WorkoutsPlus/Components/SimpleStopWatch.swift new file mode 100644 index 0000000..ae80b51 --- /dev/null +++ b/WorkoutsPlus/Components/SimpleStopWatch.swift @@ -0,0 +1,66 @@ +// +// SimpleStopWatch.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 12.09.24. +// +import SwiftUI + +struct SimpleStopWatch: View { + @State var startDate: Date + @Binding var duration: TimeInterval + + @State private var isPaused = false + @State private var timeString: String = "00:00:00" + @State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + Text(timeString) + .onReceive(timer) { _ in + if !isPaused { + duration += 1 + timeString = format(time: duration) + } + } + .onTapGesture { + isPaused.toggle() + } + .onAppear() { + duration = Date().timeIntervalSince(startDate) + timeString = format(time: duration) + startTimer() + } + .onDisappear() { + stopTimer() + } + } + + private func stopTimer() { + timer.upstream.connect().cancel() + } + + private func startTimer() { + timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + } + + private func format(time: TimeInterval) -> String { + let totalTimeInSeconds = Int(time) + + let hours = totalTimeInSeconds / 3600 + let minutes = (totalTimeInSeconds % 3600) / 60 + let seconds = totalTimeInSeconds % 60 + + var formattedTime = "" + if hours > 0 { + formattedTime += String(format: "%d:", hours) + } + formattedTime += String(format: "%02d:%02d", minutes, seconds) + + return formattedTime + } +} + +#Preview { + @Previewable @State var duration = 0.0 + SimpleStopWatch(startDate: Date.now, duration: $duration) +} diff --git a/WorkoutsPlus/Components/TimerView.swift b/WorkoutsPlus/Components/TimerView.swift deleted file mode 100644 index afd05ad..0000000 --- a/WorkoutsPlus/Components/TimerView.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TimerView.swift -// WorkoutsPlus -// -// Created by Felix Förtsch on 12.09.24. -// - -import SwiftUI - -struct TimerView: View { - @Binding var isActive: Bool - @State private var time = 0 - var startDate: Date? - let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - - var body: some View { - Text("\(time)") - .onReceive(timer) { _ in - if isActive { - if let startDate = startDate { - self.time = Int(Date.now.timeIntervalSince(startDate)) - } else { - self.time += 1 - } - } - } - .onDisappear { - self.timer.upstream.connect().cancel() - } - } -} - -#Preview { - TimerView(isActive: .constant(true), startDate: Date().addingTimeInterval(-3600)) // Example startDate 1 hour ago -} - -#Preview { - TimerView(isActive: .constant(true)) -} diff --git a/WorkoutsPlus/Configuration/Defaults.swift b/WorkoutsPlus/Configuration/Defaults.swift index 5bdb768..761b803 100644 --- a/WorkoutsPlus/Configuration/Defaults.swift +++ b/WorkoutsPlus/Configuration/Defaults.swift @@ -12,7 +12,8 @@ public class Defaults: ObservableObject { @AppStorage("isDebug") public var isDebug = true @AppStorage("isFirstAppStart") public var isFirstAppStart = true - @AppStorage("isOnboarding") public var isOnboarding = true + // TODO: isOnboarding should be true before shipping + @AppStorage("isOnboarding") public var isOnboarding = false @AppStorage("isTrainerMode") public var isTrainerMode = false @AppStorage("isWorkingOut") public var isWorkingOut = false diff --git a/WorkoutsPlus/Configuration/Protocols.swift b/WorkoutsPlus/Configuration/Protocols.swift index 77d94ef..56a991d 100644 --- a/WorkoutsPlus/Configuration/Protocols.swift +++ b/WorkoutsPlus/Configuration/Protocols.swift @@ -16,3 +16,8 @@ protocol Positionable: Identifiable { var id: UUID { get } var position: Int { get set } } + +protocol Unit { + var name: String { get } + var symbol: String { get } +} diff --git a/WorkoutsPlus/ContentView.swift b/WorkoutsPlus/ContentView.swift index 0a39f7c..f086d62 100644 --- a/WorkoutsPlus/ContentView.swift +++ b/WorkoutsPlus/ContentView.swift @@ -45,11 +45,13 @@ struct ContentView: View { } } // MARK: Workout Controls - if let activeWorkoutSession { + if let _ = activeWorkoutSession { Section { NavigationLink( destination: ActiveWorkoutSession( - activeWorkoutSession: $activeWorkoutSession) + activeWorkoutSession: Binding( + get: { activeWorkoutSession! }, + set: { activeWorkoutSession = $0 })) ) { HStack { Label("Back to Session", systemImage: "memories") @@ -61,8 +63,8 @@ struct ContentView: View { } ActiveWorkoutSessionControls( session: Binding( - get: { self.activeWorkoutSession! }, - set: { self.activeWorkoutSession = $0 } + get: { activeWorkoutSession! }, + set: { activeWorkoutSession = $0 } )) } } else { diff --git a/WorkoutsPlus/Features/Exercise/Exercise.swift b/WorkoutsPlus/Features/Exercise/Exercise.swift index 270a88e..4a038ba 100644 --- a/WorkoutsPlus/Features/Exercise/Exercise.swift +++ b/WorkoutsPlus/Features/Exercise/Exercise.swift @@ -24,15 +24,19 @@ final class Exercise: Nameable { var creationDate: Date = Date.now @Attribute(.unique) var name: String - var exerciseDescription: String = "" - var isPartOfProgression: Bool = false - var metric: ExerciseMetric + var exerciseDescription = "" + + var metric = ExerciseMetric.none + var unit: ExerciseUnit + + var isPartOfProgression = false var equipment: [Equipment] = [] init(_ name: String, _ metric: ExerciseMetric = .none) { self.name = name self.metric = metric + self.unit = ExerciseUnit(name: "Meters", symbol: "m") } } diff --git a/WorkoutsPlus/Features/Exercise/ExerciseEditor.swift b/WorkoutsPlus/Features/Exercise/ExerciseEditor.swift index edfe0b2..d599fb6 100644 --- a/WorkoutsPlus/Features/Exercise/ExerciseEditor.swift +++ b/WorkoutsPlus/Features/Exercise/ExerciseEditor.swift @@ -17,10 +17,13 @@ struct ExerciseEditor: View { @State private var name: String = "" @State private var description: String = "" - @State private var metric: ExerciseMetric = .none + @State private var metric = ExerciseMetric.none + @State private var unit = ExerciseUnit(name: "", symbol: "") @State private var isPartOfProgression: Bool = false + let exerciseUnits = ExerciseUnit.getUnits + var body: some View { NavigationStack { VStack { @@ -45,6 +48,12 @@ struct ExerciseEditor: View { } } .pickerStyle(SegmentedPickerStyle()) + Picker("Select Exercise Unit", selection: $unit) { + ForEach(exerciseUnits, id: \.self) { unit in + Text("\(unit.name) (\(unit.symbol))").tag(unit as ExerciseUnit?) + } + } + .pickerStyle(NavigationLinkPickerStyle()) } Section(footer: Text("Feature coming soon.")) { Toggle(isOn: $isPartOfProgression) { diff --git a/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift b/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift new file mode 100644 index 0000000..04df016 --- /dev/null +++ b/WorkoutsPlus/Features/Exercise/ExerciseUnit.swift @@ -0,0 +1,28 @@ +// +// ExerciseUnit.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 23.10.24. +// + +import Foundation +import SwiftData + +@Model +final class ExerciseUnit: Unit { + var name: String + var symbol: String + + init(name: String, symbol: String) { + self.name = name + self.symbol = symbol + } +} + +extension ExerciseUnit: Identifiable { + static let getUnits = [ + ExerciseUnit(name: "Kilograms", symbol: "kg"), + ExerciseUnit(name: "Kilometers", symbol: "km"), + ExerciseUnit(name: "Meters", symbol: "m"), + ] +} diff --git a/WorkoutsPlus/Features/Food/Unit.swift b/WorkoutsPlus/Features/Food/Unit.swift deleted file mode 100644 index 46f4c16..0000000 --- a/WorkoutsPlus/Features/Food/Unit.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Random.swift -// WorkoutsPlus -// -// Created by Felix Förtsch on 01.10.24. -// - -//import Foundation -// -//enum Unit: String, CaseIterable, Codable { -// case none = "" -// -// case kilogram = "kg" -// case gram = "g" -// -// case liter = "l" -// case milliliter = "ml" -// -// case kilometer = "km" -// case meter = "m" -// -// case hour = "h" -// case minute = "min" -// case second = "s" -// -// var description: String { rawValue } -//} diff --git a/WorkoutsPlus/Features/Workout/WorkoutDetail.swift b/WorkoutsPlus/Features/Workout/WorkoutDetail.swift index 2edaf72..8e6f7b3 100644 --- a/WorkoutsPlus/Features/Workout/WorkoutDetail.swift +++ b/WorkoutsPlus/Features/Workout/WorkoutDetail.swift @@ -68,22 +68,8 @@ struct WorkoutDetail: View { .navigationTitle("\(workout.name)") .toolbar { // TODO: Add proper Sharing for workouts. -// ToolbarItem() { ShareLink(item: URL(filePath: "felixfoertsch.de")!) } - if (isWorkingOut) { - Button(action: { - isWorkingOut = false - activeWorkoutSession?.stop() - }) { - HStack { - Image(systemName: "stop.fill") - Text("Stop") - } - } - .bold() - .fontDesign(.rounded) - .tint(.red) - } - else { + // ToolbarItem() { ShareLink(item: URL(filePath: "felixfoertsch.de")!) } + if (!isWorkingOut) { Button(action: { isWorkingOut = true activeWorkoutSession = workout.start() @@ -93,10 +79,11 @@ struct WorkoutDetail: View { Text("Start") } } - .bold() + .fontWeight(.bold) .fontDesign(.rounded) .tint(.green) } + // TODO: Maybe return to ActiveSessionView } .sheet(isPresented: $isPresentingWorkoutItemLibrarySheet) { WorkoutItemLibrarySheet(workout: workout) diff --git a/WorkoutsPlus/Features/Workout/WorkoutItem.swift b/WorkoutsPlus/Features/Workout/WorkoutItem.swift index 645bc96..c3153a2 100644 --- a/WorkoutsPlus/Features/Workout/WorkoutItem.swift +++ b/WorkoutsPlus/Features/Workout/WorkoutItem.swift @@ -45,6 +45,7 @@ final class WorkoutItem: Nameable, Positionable { var exercise: Exercise // Do Push-up | Run Marathon var plannedReps: Int // 8 times | 1 time var plannedValue: Double // With 10 | 42,187 + var unit: ExerciseUnit var metric: ExerciseMetric? // kg (weight) | km (distance) enum WorkoutItemType: Codable { @@ -64,7 +65,7 @@ final class WorkoutItem: Nameable, Positionable { // 0 self.plannedValue = 0 // kg - self.metric = exercise.metric + self.unit = exercise.unit } // init(set: [WorkoutItem] = []) { diff --git a/WorkoutsPlus/Features/Workout/WorkoutLibrary.swift b/WorkoutsPlus/Features/Workout/WorkoutLibrary.swift index 606bd05..4c6642c 100644 --- a/WorkoutsPlus/Features/Workout/WorkoutLibrary.swift +++ b/WorkoutsPlus/Features/Workout/WorkoutLibrary.swift @@ -10,6 +10,8 @@ import SwiftData struct WorkoutLibrary: View { @Environment(\.modelContext) private var modelContext + @Default(\.isWorkingOut) var isWorkingOut + @Binding var activeWorkoutSession: WorkoutSession? @Query(sort: \Workout.name) private var workouts: [Workout] @@ -39,11 +41,13 @@ struct WorkoutLibrary: View { Text(workout.name) } .swipeActions(edge: .leading) { - Button { - activeWorkoutSession = workout.start() - } label: { - Label("Start", systemImage: "play") - .tint(.green) + if !isWorkingOut { + Button { + activeWorkoutSession = workout.start() + } label: { + Label("Start", systemImage: "play") + .tint(.green) + } } } } diff --git a/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSession.swift b/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSession.swift index 7095078..133203c 100644 --- a/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSession.swift +++ b/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSession.swift @@ -1,43 +1,51 @@ +// +// ActiveWorkoutSession.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 12.09.24. +// import SwiftUI import SwiftData // This view can only be viewed, if there exists a WorkoutSession that can be considered active (a person working out). struct ActiveWorkoutSession: View { - @Environment(\.modelContext) private var modelContext - @Default(\.isWorkingOut) var isWorkingOut - @State var isTimerRunning: Bool = true - @Query(sort: \WorkoutSession.name) var workoutSessions: [WorkoutSession] - @Binding var activeWorkoutSession: WorkoutSession? + @Binding var activeWorkoutSession: WorkoutSession var body: some View { VStack { + VStack { + Text(activeWorkoutSession.name) + SimpleStopWatch( + startDate: activeWorkoutSession.startDate, + duration: $activeWorkoutSession.workoutDuration) + .font(.system(.largeTitle, design: .monospaced)) + .fontWeight(.bold) + } List { - Section(header: Text("Workout")) { - Text(activeWorkoutSession!.name) - } Section(header: Text("Exercises")) { - ForEach(activeWorkoutSession!.workoutSessionItems) { workoutItem in + ForEach(activeWorkoutSession.workoutSessionItems) { workoutItem in ActiveWorkoutSessionListItem(workoutItem: workoutItem) } } } } - .navigationTitle("Session") + // .navigationTitle(activeWorkoutSession.name) + // .navigationBarTitleDisplayMode(.inline) .toolbar { - Button(action: { - isWorkingOut = false - activeWorkoutSession?.stop() - }) { - HStack { - Image(systemName: "stop.fill") - Text("Stop") - } + Button(action: { + isWorkingOut = false + activeWorkoutSession.isPaused.toggle() + }) { + HStack { + Image(systemName: "stop.fill") + Text("Stop") } - .bold() - .fontDesign(.rounded) - .tint(.red) + } + .fontWeight(.bold) + .fontDesign(.rounded) + .tint(.red) } } @@ -54,8 +62,7 @@ struct ActiveWorkoutSession: View { } #Preview { - @Previewable @State var activeWorkoutSession: WorkoutSession? - @Previewable @State var workout = Workout.sampleData.first! + @Previewable @State var activeWorkoutSession = Workout.sampleData.first!.start() NavigationStack { ActiveWorkoutSession(activeWorkoutSession: $activeWorkoutSession) @@ -65,14 +72,3 @@ struct ActiveWorkoutSession: View { } .modelContainer(SampleData.shared.modelContainer) } - -//#Preview("Empty modelContainer") { -// @Previewable @State var activeWorkoutSession: WorkoutSession? -// -// NavigationStack { -// ActiveWorkoutSession(activeWorkoutSession: $activeWorkoutSession) -// } -// .onAppear { -// Defaults.shared.isWorkingOut = false -// } -//} diff --git a/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionControls.swift b/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionControls.swift index e2d6363..e958640 100644 --- a/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionControls.swift +++ b/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionControls.swift @@ -31,11 +31,11 @@ struct ActiveWorkoutSessionControls: View { } } .buttonStyle(.borderedProminent) - .bold() + .fontWeight(.bold) .tint(.primary) Button(action: { // TODO: Implement proper Pausing - session.pause() + session.isPaused = true }) { HStack { Image(systemName: "pause.fill") @@ -43,7 +43,7 @@ struct ActiveWorkoutSessionControls: View { } } .buttonStyle(.borderedProminent) - .bold() + .fontWeight(.bold) .tint(.gray) .disabled(true) Button(action: { @@ -55,7 +55,7 @@ struct ActiveWorkoutSessionControls: View { } } .buttonStyle(.borderedProminent) - .bold() + .fontWeight(.bold) .tint(.primary) } } diff --git a/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionListItem.swift b/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionListItem.swift index 6a75b55..55a56e4 100644 --- a/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionListItem.swift +++ b/WorkoutsPlus/Features/WorkoutSession/ActiveWorkoutSessionListItem.swift @@ -11,8 +11,16 @@ struct ActiveWorkoutSessionListItem: View { var workoutItem: WorkoutSessionItem var body: some View { - HStack { + VStack(alignment: .leading) { + HStack { + Text(String(workoutItem.plannedReps) + "x") + Text(String(workoutItem.plannedValue)) + Text(String(workoutItem.unit.symbol)) + if let metric = workoutItem.metric { + Text(metric.rawValue) + } Text(workoutItem.name) + .fontWeight(.bold) Spacer() Button(action: { // TODO: Implement a sheet view; don't use ExerciseDetail since its purpose is editing @@ -20,6 +28,18 @@ struct ActiveWorkoutSessionListItem: View { Image(systemName: "info.circle") .foregroundColor(.blue) } + } + HStack { + Text("Planned: ") + + if let actualRepsDone = workoutItem.actualReps { + Text("Actual: ") + Text(String(actualRepsDone)) + } else { + Text("Actual: ") + TextField("", text: .constant("")) + } + } } } } diff --git a/WorkoutsPlus/Features/WorkoutSession/WorkoutSession.swift b/WorkoutsPlus/Features/WorkoutSession/WorkoutSession.swift index fdc9a5b..2f28dae 100644 --- a/WorkoutsPlus/Features/WorkoutSession/WorkoutSession.swift +++ b/WorkoutsPlus/Features/WorkoutSession/WorkoutSession.swift @@ -24,7 +24,7 @@ final class WorkoutSession: Nameable { } // State - // var isPaused: Bool + var isPaused = false // var isCancelled: Bool // var isDeleted: Bool // var isSynced: Bool @@ -34,9 +34,10 @@ final class WorkoutSession: Nameable { // My workout session was completed at: var stopDate: Date? = nil // My workout session took me x seconds. - var duration: TimeInterval? = nil + var workoutDuration: TimeInterval = 0.0 + var pauseDuration: TimeInterval = 0.0 // My workout session is completed and moved to the workout log. - var isCompleted: Bool = false + var isCompleted = false // Exercise Progress var currentExercise = 0 @@ -46,20 +47,19 @@ final class WorkoutSession: Nameable { } // MARK: -- Workout Controls - // func start(with workout: Workout) { - // self.workout = workout - // startDate = Date.now - // } - - func pause() { - // TODO: Implement proper Pause - } +// func pause() { +// isPaused = true +// } +// +// func resume() { +// isPaused = false +// } // Call stop() to terminate the workout. func stop() { isCompleted = true stopDate = Date.now - duration = stopDate!.timeIntervalSince(startDate) +// duration = stopDate!.timeIntervalSince(startDate) } func prevExercise() { @@ -69,7 +69,7 @@ final class WorkoutSession: Nameable { } func nextExercise() { - + } // MARK: -- Workout Information diff --git a/WorkoutsPlus/Features/WorkoutSession/WorkoutSessionItem.swift b/WorkoutsPlus/Features/WorkoutSession/WorkoutSessionItem.swift index 633de35..b73df92 100644 --- a/WorkoutsPlus/Features/WorkoutSession/WorkoutSessionItem.swift +++ b/WorkoutsPlus/Features/WorkoutSession/WorkoutSessionItem.swift @@ -16,9 +16,10 @@ final class WorkoutSessionItem: Nameable, Positionable { var workoutSession: WorkoutSession - var plannedReps: Int // 8 times | 1 time - var plannedValue: Double // With 10 | 42,187 - var metric: ExerciseMetric? // kg (weight) | km (distance) + var plannedReps: Int // 8 times | 1 time + var plannedValue: Double // With 10 | 42,187 + var unit: ExerciseUnit + var metric: ExerciseMetric? // kg (weight) | km (distance) var actualReps: Int? var actualValue: Double? @@ -29,6 +30,7 @@ final class WorkoutSessionItem: Nameable, Positionable { self.position = planned.position self.plannedReps = planned.plannedReps self.plannedValue = planned.plannedValue + self.unit = planned.unit self.metric = planned.metric } } diff --git a/WorkoutsPlus/WorkoutsPlusApp.swift b/WorkoutsPlus/WorkoutsPlusApp.swift index 01a9a87..f22ba97 100644 --- a/WorkoutsPlus/WorkoutsPlusApp.swift +++ b/WorkoutsPlus/WorkoutsPlusApp.swift @@ -32,6 +32,7 @@ struct WorkoutsPlusApp: App { extension WorkoutsPlusApp { static let swiftDataSchema = Schema([ Exercise.self, + ExerciseUnit.self, Equipment.self, Workout.self, WorkoutItem.self,