From 41a82f081a3f0502bf502a3d37e4aeaba6aafa66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Fri, 27 Sep 2024 17:17:32 +0200 Subject: [PATCH] add ValueKeyboard as input for exercise values --- .../ActiveWorkoutSessionListItem.swift | 2 - .../Components/ExerciseListItem.swift | 32 ---- WorkoutsPlus/Components/SetListItem.swift | 17 +- WorkoutsPlus/Components/ValueKeyboard.swift | 175 ++++++++++++++++++ WorkoutsPlus/Exercise/ExerciseEditor.swift | 120 ++++++------ WorkoutsPlus/Models/Workout.swift | 1 - WorkoutsPlus/Models/WorkoutItem.swift | 20 +- WorkoutsPlus/Models/WorkoutSession.swift | 1 + WorkoutsPlus/README.md | 2 + WorkoutsPlus/Workout/WorkoutDetail.swift | 13 +- WorkoutsPlus/Workout/WorkoutListItem.swift | 49 +++++ 11 files changed, 308 insertions(+), 124 deletions(-) delete mode 100644 WorkoutsPlus/Components/ExerciseListItem.swift create mode 100644 WorkoutsPlus/Components/ValueKeyboard.swift create mode 100644 WorkoutsPlus/Workout/WorkoutListItem.swift diff --git a/WorkoutsPlus/ActiveWorkout/ActiveWorkoutSessionListItem.swift b/WorkoutsPlus/ActiveWorkout/ActiveWorkoutSessionListItem.swift index f3e7a0e..3491fcb 100644 --- a/WorkoutsPlus/ActiveWorkout/ActiveWorkoutSessionListItem.swift +++ b/WorkoutsPlus/ActiveWorkout/ActiveWorkoutSessionListItem.swift @@ -15,8 +15,6 @@ struct ActiveWorkoutSessionListItem: View { switch workoutItem.workoutItemType { case .set: Text(workoutItem.name) - case .workout: - Text(workoutItem.name) case .exerciseWithReps: Text(workoutItem.name) Spacer() diff --git a/WorkoutsPlus/Components/ExerciseListItem.swift b/WorkoutsPlus/Components/ExerciseListItem.swift deleted file mode 100644 index 6c8ec09..0000000 --- a/WorkoutsPlus/Components/ExerciseListItem.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ExerciseListItem.swift -// WorkoutsPlus -// -// Created by Felix Förtsch on 02.09.24. -// - -import SwiftUI - -struct ExerciseListItem: View { - var workout: Workout - @State var exercise: WorkoutItem - - init(_ workout: Workout, _ exercise: WorkoutItem ) { - self.workout = workout - self.exercise = exercise - } - - var body: some View { - Button(action: { - // workout.addExercise(from: exercise) - }) { - StepperListItem(itemName: exercise.name, itemValue: $exercise.reps) - } - } -} - -#Preview { - List { - ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups", .reps))) - } -} diff --git a/WorkoutsPlus/Components/SetListItem.swift b/WorkoutsPlus/Components/SetListItem.swift index dc46b60..98ae2a6 100644 --- a/WorkoutsPlus/Components/SetListItem.swift +++ b/WorkoutsPlus/Components/SetListItem.swift @@ -9,12 +9,7 @@ import SwiftUI struct SetListItem: View { var workout: Workout - @State var set: WorkoutItem - - init(_ workout: Workout, _ set: WorkoutItem ) { - self.workout = workout - self.set = set - } + @Binding var set: WorkoutItem var body: some View { HStack { @@ -44,7 +39,7 @@ struct SetListItem: View { } } ForEach(set.set) { workoutItem in - ExerciseListItem(workout, workoutItem) + WorkoutListItem(workout, workoutItem) .padding(.leading) } } @@ -55,18 +50,18 @@ struct SetListItem: View { } #Preview { - let set = WorkoutItem(set: [ + @Previewable @State var set = WorkoutItem(set: [ WorkoutItem(reps: 10, "Squat"), WorkoutItem(reps: 10, "Squat"), WorkoutItem(reps: 10, "Squat")]) List { - SetListItem(Workout(name: "RR"), set) + SetListItem(workout: Workout(name: "RR"), set: $set) } } #Preview("Empty Database") { - let set = WorkoutItem(set: []) + @Previewable @State var set = WorkoutItem(set: []) List { - SetListItem(Workout(name: "RR"), set) + SetListItem(workout: Workout(name: "RR"), set: $set) } } diff --git a/WorkoutsPlus/Components/ValueKeyboard.swift b/WorkoutsPlus/Components/ValueKeyboard.swift new file mode 100644 index 0000000..419c275 --- /dev/null +++ b/WorkoutsPlus/Components/ValueKeyboard.swift @@ -0,0 +1,175 @@ +// +// ValuePickerKeyboard.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 24.09.24. +// + +import SwiftUI + +protocol ValueType { + associatedtype UnitType: CaseIterable & Hashable & CustomStringConvertible + var value: String { get set } + var unit: UnitType { get set } +} + +enum ExerciseUnit: String, CaseIterable, CustomStringConvertible { + case reps = "reps" + case meter = "m" + case second = "s" + case speed = "m/s" + case pace = "s/m" + + var description: String { rawValue } +} + +struct ExerciseValue: ValueType { + var value: String = "" + var unit: ExerciseUnit = .reps +} + +enum FoodUnit: String, CaseIterable, CustomStringConvertible { + case gram = "g" + case milliliter = "ml" + + var description: String { rawValue } +} + +struct FoodValue: ValueType { + var value: String = "" + var unit: FoodUnit = .gram +} + +struct KeyboardButtonStyle: ButtonStyle { + var width: CGFloat = 100 + var height: CGFloat = 50 + var color: Color = Color(.systemGray6) + var font: Font = .system(size: 24, weight: .bold, design: .rounded) + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .frame(width: width, height: height) + .background(color) + .cornerRadius(8) + .shadow(color: .gray, radius: 1, x: 0, y: 1) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + .font(font) + } +} + +struct ValueKeyboard: View { + @Binding var isPresented: Bool + @Binding var value: Value + + var body: some View { + VStack { + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(Color(.systemGray5)) + .frame(height: 80) + + HStack { + TextField("0", text: $value.value) + .font(.system(size: 32, weight: .bold, design: .rounded)) + .padding() + + Picker("Unit", selection: $value.unit) { + ForEach(Array(Value.UnitType.allCases), id: \.self) { unit in + Text(unit.description).tag(unit) + } + } + .pickerStyle(WheelPickerStyle()) + .frame(width: 125, height: 125) + } + .padding() + .mask(RoundedRectangle(cornerRadius: 8).frame(height: 80)) + } + .padding() + .frame(height: 100) + + VStack(spacing: 20) { + HStack(spacing: 20) { + Button(action: { handleButtonTap("1") }) { + Text("1") + } + Button(action: { handleButtonTap("2") }) { + Text("2") + } + Button(action: { handleButtonTap("3") }) { + Text("3") + } + } + + HStack(spacing: 20) { + Button(action: { handleButtonTap("4") }) { + Text("4") + } + Button(action: { handleButtonTap("5") }) { + Text("5") + } + Button(action: { handleButtonTap("6") }) { + Text("6") + } + } + + HStack(spacing: 20) { + Button(action: { handleButtonTap("7") }) { + Text("7") + } + Button(action: { handleButtonTap("8") }) { + Text("8") + } + Button(action: { handleButtonTap("9") }) { + Text("9") + } + } + + HStack(spacing: 20) { + Button(action: { handleButtonTap("⌫") }) { + Text("⌫") + } + + Button(action: { handleButtonTap("0") }) { + Text("0") + } + + Button(action: { handleButtonTap("→") }) { + Text("→") + } + .buttonStyle(KeyboardButtonStyle(color: .blue)) + .foregroundStyle(.white) + } + } + .buttonStyle(KeyboardButtonStyle()) + } + } + + private func handleButtonTap(_ button: String) { + switch button { + case "⌫": + if !value.value.isEmpty { + value.value.removeLast() + } + case "→": + isPresented.toggle() + default: + value.value.append(button) + } + } +} + +#Preview("ExerciseValue") { + @Previewable @State var exerciseValue: ExerciseValue = .init() + @Previewable @State var isPresented: Bool = true + Text(exerciseValue.value) + Text(exerciseValue.unit.rawValue) + ValueKeyboard(isPresented: $isPresented, value: $exerciseValue) +} + +#Preview("FoodValue") { + @Previewable @State var foodValue: FoodValue = .init() + @Previewable @State var isPresented: Bool = true + Text(foodValue.value) + Text(foodValue.unit.rawValue) + ValueKeyboard(isPresented: $isPresented, value: $foodValue) +} diff --git a/WorkoutsPlus/Exercise/ExerciseEditor.swift b/WorkoutsPlus/Exercise/ExerciseEditor.swift index 4af2be9..ef00b1d 100644 --- a/WorkoutsPlus/Exercise/ExerciseEditor.swift +++ b/WorkoutsPlus/Exercise/ExerciseEditor.swift @@ -13,7 +13,6 @@ struct ExerciseEditor: View { @Environment(\.dismiss) private var dismiss var isPresentedAsSheet: Bool = false - // @Query(sort: \Exercise.name) var exercises: [Exercise] @State var exercise: Exercise? @State private var name: String = "" @@ -26,72 +25,75 @@ struct ExerciseEditor: View { @State private var isPartOfProgression: Bool = false + @State private var exerciseValue = ExerciseValue() + @State private var isValueKeyboardPresented = false + var body: some View { NavigationStack { - Form { - Section(footer: Text("The exercise description is optional.")) { - TextField("Exercise Name", text: $name) - // TODO: Add Autocomplete - TextField("Description", text: $description) - } - - Section(footer: Text(Exercise.getAdvice(for: name, with: metric))) { - Picker("Metric", selection: $metric) { - Text("Reps").tag(ExerciseMetric.reps) - Text("Duration").tag(ExerciseMetric.duration) - Text("Distance").tag(ExerciseMetric.distance) + VStack { + ScrollViewReader { proxy in + VStack { + Form { + Section(footer: Text("The exercise description is optional.")) { + TextField("Exercise Name", text: $name) + // TODO: Add Autocomplete + TextField("Description", text: $description) + } + Section { + Button(action: { + isValueKeyboardPresented.toggle() + proxy.scrollTo("valueButton", anchor: .center) + }, label: { + Text("\(exerciseValue.value) \(exerciseValue.unit.rawValue)") + }) + .id("valueButton") // Assign a unique ID to the button + } + Section(footer: Text("Feature coming soon.")) { + Toggle(isOn: $isPartOfProgression) { + Text("Exercise is Part of a Progression") + .foregroundStyle(.gray) + } + .disabled(true) + } + } + .navigationTitle("Edit") + .toolbar { + if isPresentedAsSheet { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel", role: .cancel) { + dismiss() + } + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + withAnimation { + save() + dismiss() + } + } + } + } } - .pickerStyle(SegmentedPickerStyle()) - switch metric { - case .reps: - RepsPicker(reps: $reps) - case .duration: - DurationPicker(duration: $duration) - case .distance: - DistancePicker(distance: $distance) - } - } - - Section(footer: Text("Feature coming soon.")) { - Toggle(isOn: $isPartOfProgression) { - Text("Exercise is Part of a Progression") - .foregroundStyle(.gray) - } - .disabled(true) - } - } - .navigationTitle("Edit") - .toolbar() { - if isPresentedAsSheet { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel", role: .cancel) { - dismiss() + .onAppear { + if let exercise { + self.name = exercise.name + self.description = exercise.exerciseDescription + + self.metric = exercise.metric + self.reps = exercise.suggestedReps + self.duration = exercise.suggestedDuration + self.distance = exercise.suggestedDistance + + self.isPartOfProgression = exercise.isPartOfProgression } } } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - withAnimation { - save() - dismiss() - } - } + if isValueKeyboardPresented { + ValueKeyboard(isPresented: $isValueKeyboardPresented, value: $exerciseValue) } } } - .onAppear() { - if let exercise { - self.name = exercise.name - self.description = exercise.exerciseDescription - - self.metric = exercise.metric - self.reps = exercise.suggestedReps - self.duration = exercise.suggestedDuration - self.distance = exercise.suggestedDistance - - self.isPartOfProgression = exercise.isPartOfProgression - } - } } private func save() { @@ -108,7 +110,7 @@ struct ExerciseEditor: View { } else { let newExercise = Exercise(name, metric) modelContext.insert(newExercise) - // try? modelContext.save() + // try? modelContext.save() } } } diff --git a/WorkoutsPlus/Models/Workout.swift b/WorkoutsPlus/Models/Workout.swift index 9b348e7..5ec6440 100644 --- a/WorkoutsPlus/Models/Workout.swift +++ b/WorkoutsPlus/Models/Workout.swift @@ -24,7 +24,6 @@ final class Workout: Nameable, Hashable { // TODO: Expected Duration (learn from past workouts with ML and predict workout duration from that - @Relationship(deleteRule: .cascade) private var workoutItems: [WorkoutItem] = [] func getWorkoutItems() -> [WorkoutItem] { return workoutItems.sorted { $0.position < $1.position } diff --git a/WorkoutsPlus/Models/WorkoutItem.swift b/WorkoutsPlus/Models/WorkoutItem.swift index 2d6d77f..c52cc1c 100644 --- a/WorkoutsPlus/Models/WorkoutItem.swift +++ b/WorkoutsPlus/Models/WorkoutItem.swift @@ -16,14 +16,20 @@ final class WorkoutItem: Nameable, Positionable { var workout: Workout? var workoutItemType: WorkoutItemType enum WorkoutItemType: Codable { + // TODO: Add workout as WorkoutItemType (needs recursive dealing) + // case workout + case rest case set - case workout case exerciseWithReps case exerciseWithDuration - case rest } var position: Int = 0 + // TODO: Wondering if a SortDescriptor in the Model is useful? + // https://old.reddit.com/r/SwiftUI/comments/1fnvkud/extracting_the_creation_of_swiftdata_query_into/ + // static var sorted: SortDescriptor { + // SortDescriptor(\.position, order: .forward) + // } var reps: Int = 0 var duration: Int = 0 @@ -90,11 +96,11 @@ extension WorkoutItem { exercises.append(WorkoutItem(exercise: exercise)) } -// var set = WorkoutItem(workoutItems: [ -// WorkoutItem(from: Exercise("Set item 1")), -// WorkoutItem(from: Exercise("Set item 2")) -// ]) -// exercises.append(set) + // var set = WorkoutItem(workoutItems: [ + // WorkoutItem(from: Exercise("Set item 1")), + // WorkoutItem(from: Exercise("Set item 2")) + // ]) + // exercises.append(set) return exercises }() diff --git a/WorkoutsPlus/Models/WorkoutSession.swift b/WorkoutsPlus/Models/WorkoutSession.swift index 5f5af52..1bddbb6 100644 --- a/WorkoutsPlus/Models/WorkoutSession.swift +++ b/WorkoutsPlus/Models/WorkoutSession.swift @@ -12,6 +12,7 @@ import SwiftData final class WorkoutSession: Nameable { var id = UUID() var name = "" + // The Workout is what *should* happen var workout: Workout? { didSet { self.name = workout?.name ?? "Unknown Workout" diff --git a/WorkoutsPlus/README.md b/WorkoutsPlus/README.md index 607f2d6..314bc1c 100644 --- a/WorkoutsPlus/README.md +++ b/WorkoutsPlus/README.md @@ -6,6 +6,8 @@ - Use Multipeer connect for sharing/tracking? - Karte mit öffentlichen Orten einbinden (AOK-Fitnesspark?) - Progression-Fotos: als eigene App? -> Generalisierung zu "Foto-Track" +- Add Protein and Kreatin-Tracker +- Exercises can have weights (dead lift with 30 kg) ## Workouts diff --git a/WorkoutsPlus/Workout/WorkoutDetail.swift b/WorkoutsPlus/Workout/WorkoutDetail.swift index fbae6d0..66691a9 100644 --- a/WorkoutsPlus/Workout/WorkoutDetail.swift +++ b/WorkoutsPlus/Workout/WorkoutDetail.swift @@ -29,18 +29,7 @@ struct WorkoutDetail: View { header: Text("Exercises"), footer: Text("Drag and drop to re-arrange or swipe to delete exercises.")) { ForEach(workout.getWorkoutItems()) { workoutItem in - switch workoutItem.workoutItemType { - case .exerciseWithReps: - ExerciseListItem(workout, workoutItem) - case .exerciseWithDuration: - ExerciseListItem(workout, workoutItem) - case .set: - SetListItem(workout, workoutItem) - case .workout: - Text(workoutItem.name) - case .rest: - Text(workoutItem.name) - } + WorkoutListItem(workout, workoutItem) } .onDelete(perform: deleteWorkoutItem) .onMove(perform: move) diff --git a/WorkoutsPlus/Workout/WorkoutListItem.swift b/WorkoutsPlus/Workout/WorkoutListItem.swift new file mode 100644 index 0000000..58f0889 --- /dev/null +++ b/WorkoutsPlus/Workout/WorkoutListItem.swift @@ -0,0 +1,49 @@ +// +// WorkoutListItem.swift +// WorkoutsPlus +// +// Created by Felix Förtsch on 02.09.24. +// + +import SwiftUI + +struct WorkoutListItem: View { + var workout: Workout + @State var workoutItem: WorkoutItem + + init(_ workout: Workout, _ workoutItem: WorkoutItem ) { + self.workout = workout + self.workoutItem = workoutItem + } + + var body: some View { + Button(action: { + // workout.addExercise(from: exercise) + }) { + switch workoutItem.workoutItemType { + case .set: + SetListItem(workout: workout, set: $workoutItem) + case .exerciseWithReps: + Text(workoutItem.name) + case .exerciseWithDuration: + Text(workoutItem.name) + case .rest: + Text(workoutItem.name) + } + } + + } +} + +#Preview { + List { + WorkoutListItem(Workout(name: "RR"), WorkoutItem(set: [ + WorkoutItem(reps: 10, "Squat"), + WorkoutItem(reps: 10, "Squat"), + WorkoutItem(reps: 10, "Squat")])) + WorkoutListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups", .reps))) + WorkoutListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups", .reps))) + WorkoutListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Sprint Interval", .duration))) + WorkoutListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Run", .distance))) + } +}