add ExerciseEditor, Picker skeletons, AutocompleteTextfield
This commit is contained in:
@@ -20,32 +20,22 @@ struct ActiveWorkoutSession: View {
|
||||
List {
|
||||
Section(header: Text("Workout"), footer: Text(activeWorkoutSession?.creationDate.ISO8601Format() ?? "Unknown Date")) {
|
||||
NavigationLink(destination: {
|
||||
ItemPicker<Workout>(items: workouts, selectedItem: $activeWorkout)
|
||||
ItemPicker<Workout>(selectedItem: $activeWorkout, items: workouts)
|
||||
}) {
|
||||
Text(activeWorkout?.name ?? "Select Workout")
|
||||
}
|
||||
.onChange(of: activeWorkout) { _, newWorkout in
|
||||
if let workout = newWorkout {
|
||||
activeWorkoutId = workout.id.uuidString
|
||||
activeWorkoutSession?.workout = workout
|
||||
if let newWorkout {
|
||||
activeWorkoutId = newWorkout.id.uuidString
|
||||
activeWorkoutSession?.workout = newWorkout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let activeWorkout = activeWorkout {
|
||||
if let activeWorkout {
|
||||
Section(header: Text("Exercises")) {
|
||||
ForEach(getActiveWorkoutItems(activeWorkout: activeWorkout)) { workoutItem in
|
||||
HStack {
|
||||
Text(String(workoutItem.reps))
|
||||
Text(workoutItem.name)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
// TODO: Implement a sheet view; don't use ExerciseDetail since its purpose is editing
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
ActiveWorkoutSessionListItem(workoutItem: workoutItem)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -54,14 +44,14 @@ struct ActiveWorkoutSession: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: -- Workout Controls
|
||||
// MARK: Workout Controls
|
||||
if (isWorkingOut) {
|
||||
if activeWorkoutSession != nil {
|
||||
ActiveWorkoutSessionControls(
|
||||
session: Binding(
|
||||
get: { self.activeWorkoutSession! },
|
||||
set: { self.activeWorkoutSession = $0 }
|
||||
))
|
||||
get: { self.activeWorkoutSession! },
|
||||
set: { self.activeWorkoutSession = $0 }
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,11 +68,14 @@ struct ActiveWorkoutSession: View {
|
||||
}
|
||||
}
|
||||
.bold()
|
||||
.fontDesign(.rounded)
|
||||
.tint(.red)
|
||||
} else {
|
||||
Button(action: {
|
||||
isWorkingOut = true
|
||||
activeWorkoutSession?.start()
|
||||
if let activeWorkout {
|
||||
activeWorkoutSession?.start(with: activeWorkout)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
@@ -90,11 +83,11 @@ struct ActiveWorkoutSession: View {
|
||||
}
|
||||
}
|
||||
.bold()
|
||||
.fontDesign(.rounded)
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Load the active workout session and workout onAppear
|
||||
if let activeWorkoutSession = getItem(from: workoutSessions, by: activeWorkoutSessionId) {
|
||||
self.activeWorkoutSession = activeWorkoutSession
|
||||
if let workout = getItem(from: workouts, by: activeWorkoutId) {
|
||||
@@ -132,6 +125,9 @@ struct ActiveWorkoutSession: View {
|
||||
NavigationStack {
|
||||
ActiveWorkoutSession(activeWorkout: activeWorkout)
|
||||
}
|
||||
.onAppear {
|
||||
Defaults.shared.isWorkingOut = false
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ struct ActiveWorkoutSessionControls: View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(session.getCurrentTodo())
|
||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||
}
|
||||
ProgressView("",
|
||||
value: session.getCurrentExerciseIndex() + 1,
|
||||
total: session.getTotalExerciseCount()
|
||||
value: session.getCurrentExerciseIndex(),
|
||||
total: session.getTotalExerciseCount() - 1
|
||||
)
|
||||
HStack {
|
||||
Button(action: {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// ActiveWorkoutSessionListItem.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 19.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActiveWorkoutSessionListItem: View {
|
||||
var workoutItem: WorkoutItem
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
switch workoutItem.workoutItemType {
|
||||
case .set:
|
||||
Text(workoutItem.name)
|
||||
case .workout:
|
||||
Text(workoutItem.name)
|
||||
case .exerciseWithReps:
|
||||
Text(workoutItem.name)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
// TODO: Implement a sheet view; don't use ExerciseDetail since its purpose is editing
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
case .exerciseWithDuration:
|
||||
Text(workoutItem.name)
|
||||
Spacer()
|
||||
Button(action: {
|
||||
// TODO: Implement a sheet view; don't use ExerciseDetail since its purpose is editing
|
||||
}) {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
case .rest:
|
||||
Text("Pause")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
ForEach(WorkoutItem.sampleDataRecommendedRoutine) { item in
|
||||
ActiveWorkoutSessionListItem(workoutItem: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
98
WorkoutsPlus/Components/AutocompleteTextField.swift
Normal file
98
WorkoutsPlus/Components/AutocompleteTextField.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// AutocompleteTextField.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 19.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AutocompleteTextField<Item: Nameable>: View {
|
||||
var placeholder: String
|
||||
|
||||
@Binding var item: Item?
|
||||
var items: [Item]
|
||||
|
||||
@State private var searchText = ""
|
||||
var filteredItems: [Item] {
|
||||
if searchText.isEmpty {
|
||||
return []
|
||||
} else {
|
||||
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
}
|
||||
|
||||
@State private var isAutoCompleteShown: Bool = true
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
HStack {
|
||||
TextField(placeholder, text: $searchText)
|
||||
// TODO: Fix "List line" not extending to the full width
|
||||
if item == nil && !searchText.isEmpty {
|
||||
Text("NEW")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
if item != nil {
|
||||
Text("EDIT")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
if item != nil {
|
||||
self.searchText = item!.name
|
||||
}
|
||||
}
|
||||
.onChange(of: searchText) {
|
||||
if filteredItems.count == 1 && filteredItems.first?.name == searchText {
|
||||
self.item = filteredItems.first
|
||||
isAutoCompleteShown = false
|
||||
} else {
|
||||
self.item = nil
|
||||
isAutoCompleteShown = true
|
||||
}
|
||||
}
|
||||
if isAutoCompleteShown {
|
||||
ForEach(filteredItems, id: \.self) { item in
|
||||
HStack {
|
||||
Text(item.name)
|
||||
.foregroundStyle(.gray)
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.left")
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
// This .contentShape makes the whole row tappable
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
self.item = item
|
||||
searchText = item.name
|
||||
isAutoCompleteShown = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Item: Nameable {
|
||||
var id = UUID()
|
||||
var name: String
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var item: Item? = nil
|
||||
VStack {
|
||||
Text("Preview only: ")
|
||||
Text(item?.name ?? "No item selected")
|
||||
}
|
||||
.background(.red)
|
||||
List {
|
||||
AutocompleteTextField<Item>(placeholder: "New Item", item: $item, items: [
|
||||
Item(name: "Item 1"),
|
||||
Item(name: "Item 2"),
|
||||
Item(name: "Item 3")
|
||||
])
|
||||
}
|
||||
}
|
||||
22
WorkoutsPlus/Components/Binding.swift
Normal file
22
WorkoutsPlus/Components/Binding.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Binding.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 21.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Binding {
|
||||
init?(_ source: Binding<Value?>) {
|
||||
guard let value = source.wrappedValue else {
|
||||
return nil
|
||||
}
|
||||
self.init(
|
||||
get: { value },
|
||||
set: { newValue in
|
||||
source.wrappedValue = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
31
WorkoutsPlus/Components/DistancePicker.swift
Normal file
31
WorkoutsPlus/Components/DistancePicker.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// DistancePicker.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 21.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DistancePicker: View {
|
||||
@Binding var distance: String
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField("Distance (m)", text: $distance)
|
||||
.keyboardType(.numberPad)
|
||||
|
||||
if let distanceInMeters = Double(distance) {
|
||||
let distanceInKilometers = distanceInMeters / 1000
|
||||
Text("Distance: \(distanceInKilometers, specifier: "%.2f") km")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var distance = ""
|
||||
DistancePicker(distance: $distance)
|
||||
}
|
||||
25
WorkoutsPlus/Components/DurationPicker.swift
Normal file
25
WorkoutsPlus/Components/DurationPicker.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// DurationPicker.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 21.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DurationPicker: View {
|
||||
@Binding var duration: String
|
||||
|
||||
var body: some View {
|
||||
// DatePicker("Duration", selection: $duration, displayedComponents: [.hourAndMinute, .date])
|
||||
VStack {
|
||||
TextField("Duration in seconds", text: $duration)
|
||||
.keyboardType(.numberPad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var duration = "120"
|
||||
DurationPicker(duration: $duration)
|
||||
}
|
||||
@@ -27,6 +27,6 @@ struct ExerciseListItem: View {
|
||||
|
||||
#Preview {
|
||||
List {
|
||||
ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups")))
|
||||
ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups", .reps)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
//
|
||||
// ItemPicker.swift
|
||||
// WorkoutsPlus
|
||||
// Advanced Version of Picker.pickerStyle(NavigationLinkPickerStyle()) that's searchable and has a ContentUnavailableView
|
||||
//
|
||||
// Created by Felix Förtsch on 10.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ItemPicker<Item: Nameable>: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Binding var selectedItem: Item?
|
||||
var items: [Item]
|
||||
|
||||
@State private var searchText = ""
|
||||
var filteredItems: [Item] {
|
||||
if searchText.isEmpty {
|
||||
@@ -20,9 +23,6 @@ struct ItemPicker<Item: Nameable>: View {
|
||||
}
|
||||
}
|
||||
|
||||
var items: [Item]
|
||||
@Binding var selectedItem: Item?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(filteredItems) { item in
|
||||
@@ -51,10 +51,18 @@ struct ItemPicker<Item: Nameable>: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var selectedWorkout: Workout? = nil
|
||||
NavigationStack {
|
||||
ItemPicker<Workout>(items: Workout.sampleData, selectedItem: $selectedWorkout)
|
||||
}
|
||||
.modelContainer(SampleData.shared.modelContainer)
|
||||
private struct Item: Nameable {
|
||||
var id = UUID()
|
||||
var name: String
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var selectedItem: Item? = nil
|
||||
NavigationStack {
|
||||
ItemPicker<Item>(selectedItem: $selectedItem, items: [
|
||||
Item(name: "Item 1"),
|
||||
Item(name: "Item 2"),
|
||||
Item(name: "Item 3")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
23
WorkoutsPlus/Components/RepsPicker.swift
Normal file
23
WorkoutsPlus/Components/RepsPicker.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// RepsPicker.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 21.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Think about implementing a custom keyboard like in FoodNoms
|
||||
struct RepsPicker: View {
|
||||
@Binding var reps: String
|
||||
|
||||
var body: some View {
|
||||
TextField("Enter reps", text: $reps)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var reps = ""
|
||||
RepsPicker(reps: $reps)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// AddExercise.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 18.08.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddExercise: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var exercise: Exercise
|
||||
|
||||
var body : some View {
|
||||
Form {
|
||||
TextField("Workout Name", text: $exercise.name)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
modelContext.delete(exercise)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
Color.clear
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
AddExercise(exercise: Exercise(""))
|
||||
}
|
||||
}
|
||||
118
WorkoutsPlus/Exercise/ExerciseEditor.swift
Normal file
118
WorkoutsPlus/Exercise/ExerciseEditor.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// AddExercise.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 21.09.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExerciseEditor: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@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 = ""
|
||||
@State private var description: String = ""
|
||||
|
||||
@State private var metric: ExerciseMetric = .reps
|
||||
@State private var reps: String = ""
|
||||
@State private var duration: String = ""
|
||||
@State private var distance: String = ""
|
||||
|
||||
@State private var isPartOfProgression: Bool = 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)
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
withAnimation {
|
||||
save()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
if let exercise {
|
||||
exercise.name = name
|
||||
exercise.exerciseDescription = description
|
||||
|
||||
exercise.metric = metric
|
||||
exercise.suggestedReps = reps
|
||||
exercise.suggestedDuration = duration
|
||||
exercise.suggestedDistance = distance
|
||||
|
||||
exercise.isPartOfProgression = isPartOfProgression
|
||||
} else {
|
||||
let newExercise = Exercise(name, metric)
|
||||
modelContext.insert(newExercise)
|
||||
// try? modelContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ExerciseEditor()
|
||||
}
|
||||
@@ -12,7 +12,7 @@ struct ExerciseLibrary: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query(sort: \Exercise.name) private var exercises: [Exercise]
|
||||
|
||||
@State private var newExercise: Exercise = Exercise("")
|
||||
@State private var newExercise: Exercise = Exercise("", .reps)
|
||||
@State private var newExerciseName: String = ""
|
||||
@State private var isAddingExercise: Bool = false
|
||||
@FocusState private var isInputFieldFocused: Bool
|
||||
@@ -26,37 +26,33 @@ struct ExerciseLibrary: View {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add search bar to the top
|
||||
|
||||
var body: some View {
|
||||
|
||||
Group {
|
||||
List {
|
||||
ForEach(filteredItems) { exercise in
|
||||
NavigationLink {
|
||||
ExerciseDetail(exercise: exercise)
|
||||
} label: {
|
||||
Text(exercise.name)
|
||||
// TODO: show exercise.metric in gray (eg Dips = reps, Intervall = time or = distance?)
|
||||
Section {
|
||||
ForEach(filteredItems) { exercise in
|
||||
NavigationLink {
|
||||
ExerciseEditor(exercise: exercise)
|
||||
} label: {
|
||||
HStack {
|
||||
Text(exercise.name)
|
||||
Spacer()
|
||||
Text(exercise.metric.rawValue)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteExercise)
|
||||
if filteredItems.isEmpty {
|
||||
ContentUnavailableView.search(text: searchText)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteExercise)
|
||||
if isAddingExercise {
|
||||
TextField("New Exercise", text: $newExerciseName, onCommit: {
|
||||
newExercise.name = newExerciseName
|
||||
save(exercise: newExercise)
|
||||
isAddingExercise = false
|
||||
})
|
||||
.textInputAutocapitalization(.words)
|
||||
.focused($isInputFieldFocused)
|
||||
Section {
|
||||
AddItemButton(label: "Exercise", action: addExercise)
|
||||
}
|
||||
if filteredItems.isEmpty {
|
||||
ContentUnavailableView.search(text: searchText)
|
||||
}
|
||||
AddItemButton(label: "Exercise", action: addExercise)
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
|
||||
}
|
||||
.navigationTitle("Exercises")
|
||||
.toolbar {
|
||||
@@ -64,27 +60,20 @@ struct ExerciseLibrary: View {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem {
|
||||
Button(action: {}) {
|
||||
Button(action: addExercise) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addExercise() {
|
||||
withAnimation {
|
||||
newExercise = Exercise("")
|
||||
newExerciseName = ""
|
||||
isAddingExercise = true
|
||||
isInputFieldFocused = true
|
||||
.sheet(isPresented: $isAddingExercise) {
|
||||
ExerciseEditor(isPresentedAsSheet: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func save(exercise: Exercise) {
|
||||
if !exercise.name.isEmpty {
|
||||
modelContext.insert(exercise)
|
||||
try? modelContext.save()
|
||||
private func addExercise() {
|
||||
withAnimation {
|
||||
isAddingExercise = true
|
||||
isInputFieldFocused = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,42 +13,76 @@ final class Exercise: Nameable {
|
||||
static var systemImage = "figure.run"
|
||||
|
||||
var id = UUID()
|
||||
var creationDate: Date = Date.now
|
||||
|
||||
// The example for a exercise is the Push-up (but could also be: a sprint interval, a jump, a marathon, etc).
|
||||
@Attribute(.unique) var name: String
|
||||
// var metric: String = "reps"
|
||||
// var exerciseDescription: ExerciseDescription?
|
||||
// Performing a push-up correctly has the following form cues
|
||||
var exerciseDescription: String = ""
|
||||
|
||||
// A push-up is measured in reps.
|
||||
var metric: ExerciseMetric
|
||||
// In a typical push-up exercise you perform 8 reps.
|
||||
var suggestedReps = "8" // TODO: Make a Rep
|
||||
var suggestedDuration = "" // TODO: Make a Duration
|
||||
var suggestedDistance = "" // TODO: Make a Distance
|
||||
|
||||
// A push-up is part of the Push-up Progression.
|
||||
var isPartOfProgression: Bool = false
|
||||
// The focus of the push-up is strength
|
||||
// var focus: ExerciseFocus? // Strength, Flexibility, Speed, etc
|
||||
|
||||
var timestamp: Date = Date.now
|
||||
|
||||
init(_ name: String = "") {
|
||||
init(_ name: String, _ metric: ExerciseMetric) {
|
||||
self.name = name
|
||||
self.metric = metric
|
||||
}
|
||||
}
|
||||
|
||||
enum ExerciseMetric: String, Codable {
|
||||
case reps = "Reps" // Repeat the exrcise for a given amount of repetitions
|
||||
case duration = "Duration" // Do the exercise for given amount of time
|
||||
case distance = "Distance" // Do the exercise for a given amount of distance
|
||||
// Other possible metrics:
|
||||
// - Open exercise: this exercise does not bring a metric (eg running)
|
||||
}
|
||||
|
||||
extension Exercise {
|
||||
static func getAdvice(for name: String, with metric: ExerciseMetric) -> String {
|
||||
switch metric {
|
||||
case .reps:
|
||||
return "Repeat \(name == "" ? "New Exercise" : name) 8 times."
|
||||
case .duration:
|
||||
return "Do \(name == "" ? "New Exercise" : name) for 30 seconds."
|
||||
case .distance:
|
||||
return "Do \(name == "" ? "New Exercise" : name) for 500 meters."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Exercise {
|
||||
static let sampleDataRecommendedRoutine: [Exercise] = [
|
||||
Exercise("Shoulder Band Warm-up"),
|
||||
Exercise("Squat Sky Reaches"),
|
||||
Exercise("GMB Wrist Prep"),
|
||||
Exercise("Dead Bugs"),
|
||||
Exercise("Pull-up Progression"),
|
||||
Exercise("Dip Progression"),
|
||||
Exercise("Squat Progression"),
|
||||
Exercise("Hinge Progression"),
|
||||
Exercise("Row Progression"),
|
||||
Exercise("Push-up Progression"),
|
||||
Exercise("Handstand Practice"),
|
||||
Exercise("Support Practice")
|
||||
Exercise("Shoulder Band Warm-up", .duration),
|
||||
Exercise("Squat Sky Reaches", .reps),
|
||||
Exercise("GMB Wrist Prep", .duration),
|
||||
Exercise("Dead Bugs", .reps),
|
||||
Exercise("Pull-up Progression", .reps),
|
||||
Exercise("Dip Progression", .reps),
|
||||
Exercise("Squat Progression", .reps),
|
||||
Exercise("Hinge Progression", .reps),
|
||||
Exercise("Row Progression", .reps),
|
||||
Exercise("Push-up Progression", .reps),
|
||||
Exercise("Handstand Practice", .duration),
|
||||
Exercise("Support Practice", .duration)
|
||||
]
|
||||
|
||||
static let sampleDataRings: [Exercise] = [
|
||||
Exercise("Dips"),
|
||||
Exercise("Chin-ups"),
|
||||
Exercise("Push-ups"),
|
||||
Exercise("Inverted Rows"),
|
||||
Exercise("Hanging Knee Raises"),
|
||||
Exercise("Pistol Squats"),
|
||||
Exercise("Hanging Leg Curls"),
|
||||
Exercise("Sissy Squats"),
|
||||
Exercise("Dips", .reps),
|
||||
Exercise("Chin-ups", .reps),
|
||||
Exercise("Push-ups", .reps),
|
||||
Exercise("Inverted Rows", .reps),
|
||||
Exercise("Hanging Knee Raises", .reps),
|
||||
Exercise("Pistol Squats", .reps),
|
||||
Exercise("Hanging Leg Curls", .reps),
|
||||
Exercise("Sissy Squats", .reps),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import Foundation
|
||||
|
||||
protocol Nameable: Identifiable, Hashable {
|
||||
var id: UUID { get }
|
||||
var name: String { get }
|
||||
var name: String { get set }
|
||||
}
|
||||
|
||||
protocol Positionable: Identifiable {
|
||||
var id: UUID { get }
|
||||
var position: Int { get }
|
||||
var position: Int { get set }
|
||||
}
|
||||
|
||||
@@ -13,14 +13,17 @@ final class Workout: Nameable, Hashable {
|
||||
static var systemImage = "figure.run.square.stack"
|
||||
|
||||
var id = UUID()
|
||||
var creationDate = Date.now
|
||||
|
||||
// The name of my workout is: Recommended Routine, My Marathon Workout
|
||||
@Attribute(.unique) var name: String
|
||||
|
||||
// Icon
|
||||
var workoutIconSystemName = "figure.run"
|
||||
var workoutIconColorName = ColorName.black
|
||||
|
||||
// Other properties and methods
|
||||
var timestamp = Date.now
|
||||
// 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] {
|
||||
@@ -134,5 +137,4 @@ extension Workout {
|
||||
|
||||
return [rr, rings]
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,14 @@ final class WorkoutItem: Nameable, Positionable {
|
||||
|
||||
var workout: Workout?
|
||||
var workoutItemType: WorkoutItemType
|
||||
enum WorkoutItemType: Codable {
|
||||
case set
|
||||
case workout
|
||||
case exerciseWithReps
|
||||
case exerciseWithDuration
|
||||
case rest
|
||||
}
|
||||
|
||||
var position: Int = 0
|
||||
|
||||
var reps: Int = 0
|
||||
@@ -31,21 +39,21 @@ final class WorkoutItem: Nameable, Positionable {
|
||||
|
||||
// Exercise
|
||||
init(reps: Int, _ exercise: String) {
|
||||
self.workoutItemType = .exercise
|
||||
self.workoutItemType = .exerciseWithReps
|
||||
self.name = exercise
|
||||
self.reps = reps
|
||||
self.exercise = Exercise(exercise)
|
||||
self.exercise = Exercise(exercise, .reps)
|
||||
}
|
||||
|
||||
init(duration: Int, _ exercise: String) {
|
||||
self.workoutItemType = .exercise
|
||||
self.workoutItemType = .exerciseWithDuration
|
||||
self.name = exercise
|
||||
self.duration = duration
|
||||
self.exercise = Exercise(exercise)
|
||||
self.exercise = Exercise(exercise, .duration)
|
||||
}
|
||||
|
||||
init(exercise: Exercise) {
|
||||
self.workoutItemType = .exercise
|
||||
self.workoutItemType = .exerciseWithReps
|
||||
self.name = exercise.name
|
||||
self.exercise = exercise
|
||||
}
|
||||
@@ -74,15 +82,6 @@ final class WorkoutItem: Nameable, Positionable {
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutItem {
|
||||
enum WorkoutItemType: Codable {
|
||||
case set
|
||||
case workout
|
||||
case exercise
|
||||
case rest
|
||||
}
|
||||
}
|
||||
|
||||
extension WorkoutItem {
|
||||
static let sampleDataRecommendedRoutine: [WorkoutItem] = {
|
||||
var exercises = [WorkoutItem]()
|
||||
|
||||
@@ -14,7 +14,7 @@ final class WorkoutSession: Nameable {
|
||||
var name = ""
|
||||
var workout: Workout? {
|
||||
didSet {
|
||||
self.name = workout?.name ?? ""
|
||||
self.name = workout?.name ?? "Unknown Workout"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +26,27 @@ final class WorkoutSession: Nameable {
|
||||
|
||||
// Time
|
||||
var creationDate = Date.now
|
||||
// My workout session started at:
|
||||
var startDate: Date? = nil
|
||||
// My workout session was completed at:
|
||||
var stopDate: Date? = nil
|
||||
// My workout session took me x seconds.
|
||||
var duration: TimeInterval? = nil
|
||||
// My workout session is completed and moved to the workout log.
|
||||
var isCompleted: Bool = false
|
||||
|
||||
// Exercise Progress
|
||||
var currentExercise = 0
|
||||
|
||||
init () { }
|
||||
|
||||
|
||||
func isActive() -> Bool {
|
||||
return startDate != nil && stopDate == nil
|
||||
}
|
||||
|
||||
// MARK: -- Workout Controls
|
||||
func start() {
|
||||
func start(with workout: Workout) {
|
||||
self.workout = workout
|
||||
startDate = Date.now
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,9 @@ struct WorkoutDetail: View {
|
||||
footer: Text("Drag and drop to re-arrange or swipe to delete exercises.")) {
|
||||
ForEach(workout.getWorkoutItems()) { workoutItem in
|
||||
switch workoutItem.workoutItemType {
|
||||
case .exercise:
|
||||
case .exerciseWithReps:
|
||||
ExerciseListItem(workout, workoutItem)
|
||||
case .exerciseWithDuration:
|
||||
ExerciseListItem(workout, workoutItem)
|
||||
case .set:
|
||||
SetListItem(workout, workoutItem)
|
||||
|
||||
@@ -22,8 +22,8 @@ struct WorkoutItemLibrarySheet: View {
|
||||
Section(header: Text("Utilities")) {
|
||||
AddItemButton(label: "Set") {
|
||||
addWorkoutItemtoWorkout(WorkoutItem(set: [
|
||||
WorkoutItem(exercise: Exercise("Set item 1")),
|
||||
WorkoutItem(exercise: Exercise("Set item 2"))
|
||||
WorkoutItem(exercise: Exercise("Set item 1", .reps)),
|
||||
WorkoutItem(exercise: Exercise("Set item 2", .reps))
|
||||
]))
|
||||
}
|
||||
AddItemButton(label: "Rest") {
|
||||
|
||||
Reference in New Issue
Block a user