add ExerciseEditor, Picker skeletons, AutocompleteTextfield

This commit is contained in:
Felix Förtsch
2024-09-23 11:41:34 +02:00
parent 41b97964c4
commit 4a42fc6c33
20 changed files with 527 additions and 164 deletions

View File

@@ -20,32 +20,22 @@ struct ActiveWorkoutSession: View {
List { List {
Section(header: Text("Workout"), footer: Text(activeWorkoutSession?.creationDate.ISO8601Format() ?? "Unknown Date")) { Section(header: Text("Workout"), footer: Text(activeWorkoutSession?.creationDate.ISO8601Format() ?? "Unknown Date")) {
NavigationLink(destination: { NavigationLink(destination: {
ItemPicker<Workout>(items: workouts, selectedItem: $activeWorkout) ItemPicker<Workout>(selectedItem: $activeWorkout, items: workouts)
}) { }) {
Text(activeWorkout?.name ?? "Select Workout") Text(activeWorkout?.name ?? "Select Workout")
} }
.onChange(of: activeWorkout) { _, newWorkout in .onChange(of: activeWorkout) { _, newWorkout in
if let workout = newWorkout { if let newWorkout {
activeWorkoutId = workout.id.uuidString activeWorkoutId = newWorkout.id.uuidString
activeWorkoutSession?.workout = workout activeWorkoutSession?.workout = newWorkout
} }
} }
} }
if let activeWorkout = activeWorkout { if let activeWorkout {
Section(header: Text("Exercises")) { Section(header: Text("Exercises")) {
ForEach(getActiveWorkoutItems(activeWorkout: activeWorkout)) { workoutItem in ForEach(getActiveWorkoutItems(activeWorkout: activeWorkout)) { workoutItem in
HStack { ActiveWorkoutSessionListItem(workoutItem: workoutItem)
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)
}
}
} }
} }
} else { } else {
@@ -54,14 +44,14 @@ struct ActiveWorkoutSession: View {
} }
} }
} }
// MARK: -- Workout Controls // MARK: Workout Controls
if (isWorkingOut) { if (isWorkingOut) {
if activeWorkoutSession != nil { if activeWorkoutSession != nil {
ActiveWorkoutSessionControls( ActiveWorkoutSessionControls(
session: Binding( session: Binding(
get: { self.activeWorkoutSession! }, get: { self.activeWorkoutSession! },
set: { self.activeWorkoutSession = $0 } set: { self.activeWorkoutSession = $0 }
)) ))
} }
} }
} }
@@ -78,11 +68,14 @@ struct ActiveWorkoutSession: View {
} }
} }
.bold() .bold()
.fontDesign(.rounded)
.tint(.red) .tint(.red)
} else { } else {
Button(action: { Button(action: {
isWorkingOut = true isWorkingOut = true
activeWorkoutSession?.start() if let activeWorkout {
activeWorkoutSession?.start(with: activeWorkout)
}
}) { }) {
HStack { HStack {
Image(systemName: "play.fill") Image(systemName: "play.fill")
@@ -90,11 +83,11 @@ struct ActiveWorkoutSession: View {
} }
} }
.bold() .bold()
.fontDesign(.rounded)
.tint(.green) .tint(.green)
} }
} }
.onAppear { .onAppear {
// Load the active workout session and workout onAppear
if let activeWorkoutSession = getItem(from: workoutSessions, by: activeWorkoutSessionId) { if let activeWorkoutSession = getItem(from: workoutSessions, by: activeWorkoutSessionId) {
self.activeWorkoutSession = activeWorkoutSession self.activeWorkoutSession = activeWorkoutSession
if let workout = getItem(from: workouts, by: activeWorkoutId) { if let workout = getItem(from: workouts, by: activeWorkoutId) {
@@ -132,6 +125,9 @@ struct ActiveWorkoutSession: View {
NavigationStack { NavigationStack {
ActiveWorkoutSession(activeWorkout: activeWorkout) ActiveWorkoutSession(activeWorkout: activeWorkout)
} }
.onAppear {
Defaults.shared.isWorkingOut = false
}
.modelContainer(SampleData.shared.modelContainer) .modelContainer(SampleData.shared.modelContainer)
} }

View File

@@ -15,10 +15,11 @@ struct ActiveWorkoutSessionControls: View {
VStack { VStack {
HStack { HStack {
Text(session.getCurrentTodo()) Text(session.getCurrentTodo())
.font(.system(size: 24, weight: .bold, design: .rounded))
} }
ProgressView("", ProgressView("",
value: session.getCurrentExerciseIndex() + 1, value: session.getCurrentExerciseIndex(),
total: session.getTotalExerciseCount() total: session.getTotalExerciseCount() - 1
) )
HStack { HStack {
Button(action: { Button(action: {

View File

@@ -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)
}
}
}

View 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")
])
}
}

View 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
}
)
}
}

View 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)
}

View 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)
}

View File

@@ -27,6 +27,6 @@ struct ExerciseListItem: View {
#Preview { #Preview {
List { List {
ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups"))) ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups", .reps)))
} }
} }

View File

@@ -1,16 +1,19 @@
// //
// ItemPicker.swift // ItemPicker.swift
// WorkoutsPlus // WorkoutsPlus
// Advanced Version of Picker.pickerStyle(NavigationLinkPickerStyle()) that's searchable and has a ContentUnavailableView
// //
// Created by Felix Förtsch on 10.09.24. // Created by Felix Förtsch on 10.09.24.
// //
import SwiftUI import SwiftUI
import SwiftData
struct ItemPicker<Item: Nameable>: View { struct ItemPicker<Item: Nameable>: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Binding var selectedItem: Item?
var items: [Item]
@State private var searchText = "" @State private var searchText = ""
var filteredItems: [Item] { var filteredItems: [Item] {
if searchText.isEmpty { if searchText.isEmpty {
@@ -20,9 +23,6 @@ struct ItemPicker<Item: Nameable>: View {
} }
} }
var items: [Item]
@Binding var selectedItem: Item?
var body: some View { var body: some View {
List { List {
ForEach(filteredItems) { item in ForEach(filteredItems) { item in
@@ -51,10 +51,18 @@ struct ItemPicker<Item: Nameable>: View {
} }
} }
#Preview { private struct Item: Nameable {
@Previewable @State var selectedWorkout: Workout? = nil var id = UUID()
NavigationStack { var name: String
ItemPicker<Workout>(items: Workout.sampleData, selectedItem: $selectedWorkout) }
}
.modelContainer(SampleData.shared.modelContainer) #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")
])
}
} }

View 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)
}

View File

@@ -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(""))
}
}

View 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()
}

View File

@@ -12,7 +12,7 @@ struct ExerciseLibrary: View {
@Environment(\.modelContext) private var modelContext @Environment(\.modelContext) private var modelContext
@Query(sort: \Exercise.name) private var exercises: [Exercise] @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 newExerciseName: String = ""
@State private var isAddingExercise: Bool = false @State private var isAddingExercise: Bool = false
@FocusState private var isInputFieldFocused: Bool @FocusState private var isInputFieldFocused: Bool
@@ -26,37 +26,33 @@ struct ExerciseLibrary: View {
} }
} }
// TODO: Add search bar to the top
var body: some View { var body: some View {
Group { Group {
List { List {
ForEach(filteredItems) { exercise in Section {
NavigationLink { ForEach(filteredItems) { exercise in
ExerciseDetail(exercise: exercise) NavigationLink {
} label: { ExerciseEditor(exercise: exercise)
Text(exercise.name) } label: {
// TODO: show exercise.metric in gray (eg Dips = reps, Intervall = time or = distance?) 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) Section {
if isAddingExercise { AddItemButton(label: "Exercise", action: addExercise)
TextField("New Exercise", text: $newExerciseName, onCommit: {
newExercise.name = newExerciseName
save(exercise: newExercise)
isAddingExercise = false
})
.textInputAutocapitalization(.words)
.focused($isInputFieldFocused)
} }
if filteredItems.isEmpty {
ContentUnavailableView.search(text: searchText)
}
AddItemButton(label: "Exercise", action: addExercise)
} }
.searchable(text: $searchText) .searchable(text: $searchText)
} }
.navigationTitle("Exercises") .navigationTitle("Exercises")
.toolbar { .toolbar {
@@ -64,27 +60,20 @@ struct ExerciseLibrary: View {
EditButton() EditButton()
} }
ToolbarItem { ToolbarItem {
Button(action: {}) { Button(action: addExercise) {
Image(systemName: "plus") Image(systemName: "plus")
} }
} }
} }
} .sheet(isPresented: $isAddingExercise) {
ExerciseEditor(isPresentedAsSheet: true)
private func addExercise() {
withAnimation {
newExercise = Exercise("")
newExerciseName = ""
isAddingExercise = true
isInputFieldFocused = true
} }
} }
private func save(exercise: Exercise) { private func addExercise() {
if !exercise.name.isEmpty { withAnimation {
modelContext.insert(exercise) isAddingExercise = true
try? modelContext.save() isInputFieldFocused = true
} }
} }

View File

@@ -13,42 +13,76 @@ final class Exercise: Nameable {
static var systemImage = "figure.run" static var systemImage = "figure.run"
var id = UUID() 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 @Attribute(.unique) var name: String
// var metric: String = "reps" // Performing a push-up correctly has the following form cues
// var exerciseDescription: ExerciseDescription? 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 focus: ExerciseFocus? // Strength, Flexibility, Speed, etc
var timestamp: Date = Date.now init(_ name: String, _ metric: ExerciseMetric) {
init(_ name: String = "") {
self.name = name 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 { extension Exercise {
static let sampleDataRecommendedRoutine: [Exercise] = [ static let sampleDataRecommendedRoutine: [Exercise] = [
Exercise("Shoulder Band Warm-up"), Exercise("Shoulder Band Warm-up", .duration),
Exercise("Squat Sky Reaches"), Exercise("Squat Sky Reaches", .reps),
Exercise("GMB Wrist Prep"), Exercise("GMB Wrist Prep", .duration),
Exercise("Dead Bugs"), Exercise("Dead Bugs", .reps),
Exercise("Pull-up Progression"), Exercise("Pull-up Progression", .reps),
Exercise("Dip Progression"), Exercise("Dip Progression", .reps),
Exercise("Squat Progression"), Exercise("Squat Progression", .reps),
Exercise("Hinge Progression"), Exercise("Hinge Progression", .reps),
Exercise("Row Progression"), Exercise("Row Progression", .reps),
Exercise("Push-up Progression"), Exercise("Push-up Progression", .reps),
Exercise("Handstand Practice"), Exercise("Handstand Practice", .duration),
Exercise("Support Practice") Exercise("Support Practice", .duration)
] ]
static let sampleDataRings: [Exercise] = [ static let sampleDataRings: [Exercise] = [
Exercise("Dips"), Exercise("Dips", .reps),
Exercise("Chin-ups"), Exercise("Chin-ups", .reps),
Exercise("Push-ups"), Exercise("Push-ups", .reps),
Exercise("Inverted Rows"), Exercise("Inverted Rows", .reps),
Exercise("Hanging Knee Raises"), Exercise("Hanging Knee Raises", .reps),
Exercise("Pistol Squats"), Exercise("Pistol Squats", .reps),
Exercise("Hanging Leg Curls"), Exercise("Hanging Leg Curls", .reps),
Exercise("Sissy Squats"), Exercise("Sissy Squats", .reps),
] ]
} }

View File

@@ -9,10 +9,10 @@ import Foundation
protocol Nameable: Identifiable, Hashable { protocol Nameable: Identifiable, Hashable {
var id: UUID { get } var id: UUID { get }
var name: String { get } var name: String { get set }
} }
protocol Positionable: Identifiable { protocol Positionable: Identifiable {
var id: UUID { get } var id: UUID { get }
var position: Int { get } var position: Int { get set }
} }

View File

@@ -13,14 +13,17 @@ final class Workout: Nameable, Hashable {
static var systemImage = "figure.run.square.stack" static var systemImage = "figure.run.square.stack"
var id = UUID() var id = UUID()
var creationDate = Date.now
// The name of my workout is: Recommended Routine, My Marathon Workout
@Attribute(.unique) var name: String @Attribute(.unique) var name: String
// Icon // Icon
var workoutIconSystemName = "figure.run" var workoutIconSystemName = "figure.run"
var workoutIconColorName = ColorName.black var workoutIconColorName = ColorName.black
// Other properties and methods // TODO: Expected Duration (learn from past workouts with ML and predict workout duration from that
var timestamp = Date.now
@Relationship(deleteRule: .cascade) private var workoutItems: [WorkoutItem] = [] @Relationship(deleteRule: .cascade) private var workoutItems: [WorkoutItem] = []
func getWorkoutItems() -> [WorkoutItem] { func getWorkoutItems() -> [WorkoutItem] {
@@ -134,5 +137,4 @@ extension Workout {
return [rr, rings] return [rr, rings]
}() }()
} }

View File

@@ -15,6 +15,14 @@ final class WorkoutItem: Nameable, Positionable {
var workout: Workout? var workout: Workout?
var workoutItemType: WorkoutItemType var workoutItemType: WorkoutItemType
enum WorkoutItemType: Codable {
case set
case workout
case exerciseWithReps
case exerciseWithDuration
case rest
}
var position: Int = 0 var position: Int = 0
var reps: Int = 0 var reps: Int = 0
@@ -31,21 +39,21 @@ final class WorkoutItem: Nameable, Positionable {
// Exercise // Exercise
init(reps: Int, _ exercise: String) { init(reps: Int, _ exercise: String) {
self.workoutItemType = .exercise self.workoutItemType = .exerciseWithReps
self.name = exercise self.name = exercise
self.reps = reps self.reps = reps
self.exercise = Exercise(exercise) self.exercise = Exercise(exercise, .reps)
} }
init(duration: Int, _ exercise: String) { init(duration: Int, _ exercise: String) {
self.workoutItemType = .exercise self.workoutItemType = .exerciseWithDuration
self.name = exercise self.name = exercise
self.duration = duration self.duration = duration
self.exercise = Exercise(exercise) self.exercise = Exercise(exercise, .duration)
} }
init(exercise: Exercise) { init(exercise: Exercise) {
self.workoutItemType = .exercise self.workoutItemType = .exerciseWithReps
self.name = exercise.name self.name = exercise.name
self.exercise = exercise 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 { extension WorkoutItem {
static let sampleDataRecommendedRoutine: [WorkoutItem] = { static let sampleDataRecommendedRoutine: [WorkoutItem] = {
var exercises = [WorkoutItem]() var exercises = [WorkoutItem]()

View File

@@ -14,7 +14,7 @@ final class WorkoutSession: Nameable {
var name = "" var name = ""
var workout: Workout? { var workout: Workout? {
didSet { didSet {
self.name = workout?.name ?? "" self.name = workout?.name ?? "Unknown Workout"
} }
} }
@@ -26,22 +26,27 @@ final class WorkoutSession: Nameable {
// Time // Time
var creationDate = Date.now var creationDate = Date.now
// My workout session started at:
var startDate: Date? = nil var startDate: Date? = nil
// My workout session was completed at:
var stopDate: Date? = nil var stopDate: Date? = nil
// My workout session took me x seconds.
var duration: TimeInterval? = nil var duration: TimeInterval? = nil
// My workout session is completed and moved to the workout log.
var isCompleted: Bool = false var isCompleted: Bool = false
// Exercise Progress // Exercise Progress
var currentExercise = 0 var currentExercise = 0
init () { } init () { }
func isActive() -> Bool { func isActive() -> Bool {
return startDate != nil && stopDate == nil return startDate != nil && stopDate == nil
} }
// MARK: -- Workout Controls // MARK: -- Workout Controls
func start() { func start(with workout: Workout) {
self.workout = workout
startDate = Date.now startDate = Date.now
} }

View File

@@ -30,7 +30,9 @@ struct WorkoutDetail: View {
footer: Text("Drag and drop to re-arrange or swipe to delete exercises.")) { footer: Text("Drag and drop to re-arrange or swipe to delete exercises.")) {
ForEach(workout.getWorkoutItems()) { workoutItem in ForEach(workout.getWorkoutItems()) { workoutItem in
switch workoutItem.workoutItemType { switch workoutItem.workoutItemType {
case .exercise: case .exerciseWithReps:
ExerciseListItem(workout, workoutItem)
case .exerciseWithDuration:
ExerciseListItem(workout, workoutItem) ExerciseListItem(workout, workoutItem)
case .set: case .set:
SetListItem(workout, workoutItem) SetListItem(workout, workoutItem)

View File

@@ -22,8 +22,8 @@ struct WorkoutItemLibrarySheet: View {
Section(header: Text("Utilities")) { Section(header: Text("Utilities")) {
AddItemButton(label: "Set") { AddItemButton(label: "Set") {
addWorkoutItemtoWorkout(WorkoutItem(set: [ addWorkoutItemtoWorkout(WorkoutItem(set: [
WorkoutItem(exercise: Exercise("Set item 1")), WorkoutItem(exercise: Exercise("Set item 1", .reps)),
WorkoutItem(exercise: Exercise("Set item 2")) WorkoutItem(exercise: Exercise("Set item 2", .reps))
])) ]))
} }
AddItemButton(label: "Rest") { AddItemButton(label: "Rest") {