add ValueKeyboard as input for exercise values
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
175
WorkoutsPlus/Components/ValueKeyboard.swift
Normal file
175
WorkoutsPlus/Components/ValueKeyboard.swift
Normal file
@@ -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<Value: ValueType>: 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<WorkoutItem> {
|
||||
// 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
|
||||
}()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
49
WorkoutsPlus/Workout/WorkoutListItem.swift
Normal file
49
WorkoutsPlus/Workout/WorkoutListItem.swift
Normal file
@@ -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)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user