create ER diagram, refactor to conform to diagram, simplify session management
This commit is contained in:
@@ -195,6 +195,8 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 3F595B132C67A6AB00C4544B;
|
mainGroup = 3F595B132C67A6AB00C4544B;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 3F595B1D2C67A6AB00C4544B /* Products */;
|
productRefGroup = 3F595B1D2C67A6AB00C4544B /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
|||||||
@@ -8,50 +8,21 @@ struct ActiveWorkoutSession: View {
|
|||||||
@State var isTimerRunning: Bool = true
|
@State var isTimerRunning: Bool = true
|
||||||
|
|
||||||
@Query(sort: \WorkoutSession.name) var workoutSessions: [WorkoutSession]
|
@Query(sort: \WorkoutSession.name) var workoutSessions: [WorkoutSession]
|
||||||
@State var activeWorkoutSession: WorkoutSession?
|
@Binding var activeWorkoutSession: WorkoutSession?
|
||||||
@Default(\.activeWorkoutSessionId) var activeWorkoutSessionId
|
|
||||||
|
|
||||||
@Query(sort: \Workout.name) var workouts: [Workout]
|
@Query(sort: \Workout.name) var workouts: [Workout]
|
||||||
@State var activeWorkout: Workout?
|
@State var activeWorkout: Workout
|
||||||
@Default(\.activeWorkoutId) var activeWorkoutId
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
List {
|
List {
|
||||||
Section(header: Text("Workout"), footer: Text(activeWorkoutSession?.creationDate.ISO8601Format() ?? "Unknown Date")) {
|
Section(header: Text("Workout")) {
|
||||||
NavigationLink(destination: {
|
Text(activeWorkout.name)
|
||||||
ItemPicker<Workout>(selectedItem: $activeWorkout, items: workouts)
|
|
||||||
}) {
|
|
||||||
Text(activeWorkout?.name ?? "Select Workout")
|
|
||||||
}
|
|
||||||
.onChange(of: activeWorkout) { _, newWorkout in
|
|
||||||
if let newWorkout {
|
|
||||||
activeWorkoutId = newWorkout.id.uuidString
|
|
||||||
activeWorkoutSession?.workout = newWorkout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Section(header: Text("Exercises")) {
|
||||||
if let activeWorkout {
|
ForEach(getActiveWorkoutItems(activeWorkout: activeWorkout)) { workoutItem in
|
||||||
Section(header: Text("Exercises")) {
|
ActiveWorkoutSessionListItem(workoutItem: workoutItem)
|
||||||
ForEach(getActiveWorkoutItems(activeWorkout: activeWorkout)) { workoutItem in
|
|
||||||
ActiveWorkoutSessionListItem(workoutItem: workoutItem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ContentUnavailableView {
|
|
||||||
Label("Select a Workout", systemImage: "arrow.up")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// MARK: Workout Controls
|
|
||||||
if (isWorkingOut) {
|
|
||||||
if activeWorkoutSession != nil {
|
|
||||||
ActiveWorkoutSessionControls(
|
|
||||||
session: Binding(
|
|
||||||
get: { self.activeWorkoutSession! },
|
|
||||||
set: { self.activeWorkoutSession = $0 }
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +41,11 @@ struct ActiveWorkoutSession: View {
|
|||||||
.bold()
|
.bold()
|
||||||
.fontDesign(.rounded)
|
.fontDesign(.rounded)
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
isWorkingOut = true
|
isWorkingOut = true
|
||||||
if let activeWorkout {
|
activeWorkoutSession = activeWorkout.start()
|
||||||
activeWorkoutSession?.start(with: activeWorkout)
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "play.fill")
|
Image(systemName: "play.fill")
|
||||||
@@ -87,25 +57,6 @@ struct ActiveWorkoutSession: View {
|
|||||||
.tint(.green)
|
.tint(.green)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
if let activeWorkoutSession = getItem(from: workoutSessions, by: activeWorkoutSessionId) {
|
|
||||||
self.activeWorkoutSession = activeWorkoutSession
|
|
||||||
if let workout = getItem(from: workouts, by: activeWorkoutId) {
|
|
||||||
self.activeWorkout = workout
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createNewWorkoutSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createNewWorkoutSession() {
|
|
||||||
let newWorkoutSession = WorkoutSession()
|
|
||||||
activeWorkoutSessionId = newWorkoutSession.id.uuidString
|
|
||||||
if let selectedWorkout = getItem(from: workouts, by: activeWorkoutId) {
|
|
||||||
newWorkoutSession.workout = selectedWorkout
|
|
||||||
}
|
|
||||||
self.activeWorkoutSession = newWorkoutSession
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getActiveWorkoutItems(activeWorkout: Workout?) -> [WorkoutItem] {
|
private func getActiveWorkoutItems(activeWorkout: Workout?) -> [WorkoutItem] {
|
||||||
@@ -120,10 +71,12 @@ struct ActiveWorkoutSession: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("RR Selected") {
|
#Preview {
|
||||||
@Previewable @State var activeWorkout = Workout.sampleData.first!
|
@Previewable @State var activeWorkoutSession: WorkoutSession?
|
||||||
|
@Previewable @State var workout = Workout.sampleData.first!
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ActiveWorkoutSession(activeWorkout: activeWorkout)
|
ActiveWorkoutSession(activeWorkoutSession: $activeWorkoutSession, activeWorkout: workout)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
Defaults.shared.isWorkingOut = false
|
Defaults.shared.isWorkingOut = false
|
||||||
@@ -131,18 +84,13 @@ struct ActiveWorkoutSession: View {
|
|||||||
.modelContainer(SampleData.shared.modelContainer)
|
.modelContainer(SampleData.shared.modelContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("No Workout Selected") {
|
//#Preview("Empty modelContainer") {
|
||||||
NavigationStack {
|
// @Previewable @State var activeWorkoutSession: WorkoutSession?
|
||||||
ActiveWorkoutSession()
|
//
|
||||||
}
|
// NavigationStack {
|
||||||
.onAppear {
|
// ActiveWorkoutSession(activeWorkoutSession: $activeWorkoutSession)
|
||||||
Defaults.shared.isWorkingOut = false
|
// }
|
||||||
}
|
// .onAppear {
|
||||||
.modelContainer(SampleData.shared.modelContainer)
|
// Defaults.shared.isWorkingOut = false
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
#Preview("No Workout Data available") {
|
|
||||||
NavigationStack {
|
|
||||||
ActiveWorkoutSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ struct ActiveWorkoutSessionControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview("isWorkingOut = true") {
|
#Preview("isWorkingOut = true") {
|
||||||
@Previewable @State var activeWorkoutSession = WorkoutSession()
|
@Previewable @State var activeWorkoutSession = WorkoutSession(start: Workout.sampleData.first!)
|
||||||
activeWorkoutSession.workout = Workout.sampleData.first!
|
|
||||||
|
|
||||||
// For some reason the return keyword is required here to avoid the error "Type of expression is ambiguous without a type annotation"
|
// For some reason the return keyword is required here to avoid the error "Type of expression is ambiguous without a type annotation"
|
||||||
return ActiveWorkoutSessionControls(session: $activeWorkoutSession)
|
return ActiveWorkoutSessionControls(session: $activeWorkoutSession)
|
||||||
@@ -72,12 +71,3 @@ struct ActiveWorkoutSessionControls: View {
|
|||||||
Defaults.shared.isWorkingOut = true
|
Defaults.shared.isWorkingOut = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("isWorkingOut = false") {
|
|
||||||
@Previewable @State var activeWorkoutSession = WorkoutSession()
|
|
||||||
|
|
||||||
return ActiveWorkoutSessionControls(session: $activeWorkoutSession)
|
|
||||||
.onAppear() {
|
|
||||||
Defaults.shared.isWorkingOut = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
59
WorkoutsPlus/Components/NumberOnlyTextField.swift
Normal file
59
WorkoutsPlus/Components/NumberOnlyTextField.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//
|
||||||
|
// DurationPicker.swift
|
||||||
|
// WorkoutsPlus
|
||||||
|
//
|
||||||
|
// Created by Felix Förtsch on 06.10.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NumbersOnlyTextField: View {
|
||||||
|
@Binding var value: Double
|
||||||
|
@FocusState private var isFocused: Bool
|
||||||
|
|
||||||
|
var placeholder = "00:00"
|
||||||
|
var formatString = "%.0f"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
ZStack {
|
||||||
|
Rectangle()
|
||||||
|
.fill(.quaternary)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.frame(width: placeholderWidth())
|
||||||
|
|
||||||
|
TextField(placeholder, text: Binding(
|
||||||
|
get: { String(format: formatString, value) },
|
||||||
|
set: { newValue in
|
||||||
|
if let doubleValue = Double(newValue.filter { "0123456789.".contains($0) }) {
|
||||||
|
value = doubleValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.frame(width: placeholderWidth())
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.focused($isFocused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
isFocused = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func placeholderWidth() -> CGFloat {
|
||||||
|
let font = UIFont.systemFont(ofSize: UIFont.systemFontSize)
|
||||||
|
let attributes = [NSAttributedString.Key.font: font]
|
||||||
|
let size = (placeholder as NSString).size(withAttributes: attributes)
|
||||||
|
return size.width + 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
@Previewable @State var value: Double = 0
|
||||||
|
List {
|
||||||
|
NumbersOnlyTextField(value: $value)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
WorkoutsPlus/Components/RestListItem.swift
Normal file
25
WorkoutsPlus/Components/RestListItem.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// RestListItem.swift
|
||||||
|
// WorkoutsPlus
|
||||||
|
//
|
||||||
|
// Created by Felix Förtsch on 02.10.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RestListItem: View {
|
||||||
|
var restTime: TimeInterval = 15
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "pause")
|
||||||
|
Text("Rest for \(Int(restTime.rounded())) seconds")
|
||||||
|
.font(.system(size: 14, weight: .regular, design: .default))
|
||||||
|
.textCase(.uppercase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
RestListItem()
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ struct SetListItem: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
HStack {
|
HStack {
|
||||||
Text(String(set.reps))
|
Text(String(set.plannedReps))
|
||||||
.font(.system(size: 14, weight: .bold))
|
.font(.system(size: 14, weight: .bold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
.frame(width: 20, height: 10)
|
.frame(width: 20, height: 10)
|
||||||
@@ -27,7 +27,7 @@ struct SetListItem: View {
|
|||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
Spacer()
|
Spacer()
|
||||||
Stepper(
|
Stepper(
|
||||||
value: $set.reps,
|
value: $set.plannedReps,
|
||||||
in: 0...100,
|
in: 0...100,
|
||||||
step: 1
|
step: 1
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ public class Defaults: ObservableObject {
|
|||||||
@AppStorage("sets") public var sets = 8
|
@AppStorage("sets") public var sets = 8
|
||||||
@AppStorage("reps") public var reps = 8
|
@AppStorage("reps") public var reps = 8
|
||||||
|
|
||||||
@AppStorage("activeWorkoutSessionId") public var activeWorkoutSessionId: String = ""
|
// TODO: Maybe store session and workout ids to recover from app close
|
||||||
@AppStorage("activeWorkoutId") public var activeWorkoutId: String = ""
|
// @AppStorage("activeWorkoutSessionId") public var activeWorkoutSessionId: String = ""
|
||||||
|
// @AppStorage("activeWorkoutId") public var activeWorkoutId: String = ""
|
||||||
|
|
||||||
public static let shared = Defaults()
|
public static let shared = Defaults()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ struct ContentView: View {
|
|||||||
@Default(\.isOnboarding) var isOnboarding
|
@Default(\.isOnboarding) var isOnboarding
|
||||||
@Default(\.isWorkingOut) var isWorkingOut
|
@Default(\.isWorkingOut) var isWorkingOut
|
||||||
|
|
||||||
|
@State var activeWorkoutSession: WorkoutSession?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
List {
|
List {
|
||||||
@@ -42,9 +44,14 @@ struct ContentView: View {
|
|||||||
// .frame(height: 200)
|
// .frame(height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section {
|
// MARK: Workout Controls
|
||||||
NavigationLink(destination: ActiveWorkoutSession()) {
|
if let activeWorkoutSession {
|
||||||
if (isWorkingOut) {
|
Section {
|
||||||
|
NavigationLink(
|
||||||
|
destination: ActiveWorkoutSession(
|
||||||
|
activeWorkoutSession: $activeWorkoutSession,
|
||||||
|
activeWorkout: activeWorkoutSession.workout)
|
||||||
|
) {
|
||||||
HStack {
|
HStack {
|
||||||
Label("Back to Session", systemImage: "memories")
|
Label("Back to Session", systemImage: "memories")
|
||||||
.symbolEffect(.rotate)
|
.symbolEffect(.rotate)
|
||||||
@@ -52,14 +59,25 @@ struct ContentView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Label("Start Workout Session", systemImage: "play")
|
|
||||||
}
|
}
|
||||||
|
ActiveWorkoutSessionControls(
|
||||||
|
session: Binding(
|
||||||
|
get: { self.activeWorkoutSession! },
|
||||||
|
set: { self.activeWorkoutSession = $0 }
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Section(footer: Text("Starts the next planned workout immediately.")) {
|
||||||
|
Label("Quick Start", systemImage: "play")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Section {
|
||||||
NavigationLink(destination: WorkoutLog()) {
|
NavigationLink(destination: WorkoutLog()) {
|
||||||
Label("Workout Log", systemImage: "calendar.badge.clock")
|
Label("Workout Log", systemImage: "calendar.badge.clock")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: WorkoutLibrary()) {
|
NavigationLink(destination: WorkoutLibrary(activeWorkoutSession: $activeWorkoutSession)) {
|
||||||
Label("Workouts", systemImage: "figure.run.square.stack")
|
Label("Workouts", systemImage: "figure.run.square.stack")
|
||||||
}
|
}
|
||||||
NavigationLink(destination: ExerciseLibrary()) {
|
NavigationLink(destination: ExerciseLibrary()) {
|
||||||
@@ -67,9 +85,11 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Section {
|
Section {
|
||||||
Label("Add Food", systemImage: "plus")
|
|
||||||
Label("Food Log", systemImage: "list.bullet.clipboard")
|
Label("Food Log", systemImage: "list.bullet.clipboard")
|
||||||
Label("Food Library", systemImage: "fork.knife")
|
NavigationLink(destination: Label("Add Food", systemImage: "plus")) {
|
||||||
|
Label("Food Library", systemImage: "fork.knife")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(destination: Settings()) {
|
NavigationLink(destination: Settings()) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ struct DebugList: View {
|
|||||||
Section(header: Text("WorkoutItems")) {
|
Section(header: Text("WorkoutItems")) {
|
||||||
ForEach(workoutItems) { workoutItem in
|
ForEach(workoutItems) { workoutItem in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("\(workoutItem.name), pos: \(workoutItem.position), reps: \(workoutItem.reps)")
|
Text("\(workoutItem.name), pos: \(workoutItem.position), reps: \(workoutItem.plannedReps)")
|
||||||
Text(workoutItem.id.uuidString)
|
Text(workoutItem.id.uuidString)
|
||||||
.font(.system(size: 12, weight: .bold))
|
.font(.system(size: 12, weight: .bold))
|
||||||
.fontDesign(.monospaced)
|
.fontDesign(.monospaced)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import SwiftData
|
|||||||
// .safeAreaInset(edge: .bottom) { }
|
// .safeAreaInset(edge: .bottom) { }
|
||||||
struct MiniPlayer: View {
|
struct MiniPlayer: View {
|
||||||
@Default(\.isWorkingOut) var isWorkingOut
|
@Default(\.isWorkingOut) var isWorkingOut
|
||||||
@Default(\.activeWorkoutId) var selectedWorkoutId
|
|
||||||
|
|
||||||
// @Query private var workouts: [Workout]
|
// @Query private var workouts: [Workout]
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ struct MiniPlayer: View {
|
|||||||
Text("Start Workout")
|
Text("Start Workout")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
// TODO: Replace this with the upcoming/planned workout
|
// TODO: Replace this with the upcoming/planned workout
|
||||||
Text(selectedWorkoutId)
|
// Text(selectedWorkoutId)
|
||||||
.font(.subheadline)
|
// .font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
// .foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|||||||
@@ -74,11 +74,9 @@ struct WorkoutLog: View {
|
|||||||
Section(header: Text("Workout Sessions")) {
|
Section(header: Text("Workout Sessions")) {
|
||||||
ForEach(workoutSessions) { session in
|
ForEach(workoutSessions) { session in
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text(session.creationDate.ISO8601Format())
|
Text(session.startDate.ISO8601Format())
|
||||||
if let workout = session.workout {
|
Text(session.workout.name)
|
||||||
Text(workout.name)
|
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.onDelete(perform: deleteWorkoutSession)
|
}.onDelete(perform: deleteWorkoutSession)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ final class Workout: Nameable, Hashable {
|
|||||||
|
|
||||||
// The name of my workout is: Recommended Routine, My Marathon Workout
|
// The name of my workout is: Recommended Routine, My Marathon Workout
|
||||||
@Attribute(.unique) var name: String
|
@Attribute(.unique) var name: String
|
||||||
|
var defaultRestTime: TimeInterval = 60
|
||||||
|
var useDefaultRestTime: Bool = false
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
var workoutIconSystemName = "figure.run"
|
var workoutIconSystemName = "figure.run"
|
||||||
@@ -33,6 +35,10 @@ final class Workout: Nameable, Hashable {
|
|||||||
self.name = name
|
self.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func start() -> WorkoutSession {
|
||||||
|
return WorkoutSession(start: self)
|
||||||
|
}
|
||||||
|
|
||||||
func add(workoutItem: WorkoutItem) {
|
func add(workoutItem: WorkoutItem) {
|
||||||
self.workoutItems.append(workoutItem)
|
self.workoutItems.append(workoutItem)
|
||||||
updateWorkoutItemsPositions()
|
updateWorkoutItemsPositions()
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ final class WorkoutItem: Nameable, Positionable {
|
|||||||
var position: Int = 0
|
var position: Int = 0
|
||||||
var set: [WorkoutItem] = []
|
var set: [WorkoutItem] = []
|
||||||
|
|
||||||
var exercise: Exercise? // Do Push-up | Run Marathon
|
// Exercise has to be optional to allow Rest and Set to be a WorkoutItem (without being an Exercise).
|
||||||
var reps: Int // 8 times | 1 time
|
var exerciseData: Exercise? // Do Push-up | Run Marathon
|
||||||
var value: Double? // With 10 | 42,187
|
var plannedReps: Int // 8 times | 1 time
|
||||||
|
var plannedValue: Double // With 10 | 42,187
|
||||||
var metric: ExerciseMetric? // kg (weight) | km (distance)
|
var metric: ExerciseMetric? // kg (weight) | km (distance)
|
||||||
|
|
||||||
enum WorkoutItemType: Codable {
|
enum WorkoutItemType: Codable {
|
||||||
@@ -53,15 +54,15 @@ final class WorkoutItem: Nameable, Positionable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(_ exercise: Exercise) {
|
init(_ exercise: Exercise) {
|
||||||
self.exercise = exercise
|
self.exerciseData = exercise
|
||||||
self.workoutItemType = .exercise
|
self.workoutItemType = .exercise
|
||||||
|
|
||||||
// Push-up
|
// Push-up
|
||||||
self.name = exercise.name
|
self.name = exercise.name
|
||||||
// 8x
|
// 8x
|
||||||
self.reps = 1
|
self.plannedReps = 1
|
||||||
// 0
|
// 0
|
||||||
self.value = 0
|
self.plannedValue = 0
|
||||||
// kg
|
// kg
|
||||||
self.metric = exercise.metric
|
self.metric = exercise.metric
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,8 @@ final class WorkoutItem: Nameable, Positionable {
|
|||||||
init(set: [WorkoutItem] = []) {
|
init(set: [WorkoutItem] = []) {
|
||||||
self.workoutItemType = .set
|
self.workoutItemType = .set
|
||||||
self.name = "Set"
|
self.name = "Set"
|
||||||
self.reps = 3
|
self.plannedReps = 3
|
||||||
|
self.plannedValue = 0
|
||||||
set.forEach(addChild)
|
set.forEach(addChild)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +84,8 @@ final class WorkoutItem: Nameable, Positionable {
|
|||||||
init(rest: Double) {
|
init(rest: Double) {
|
||||||
self.workoutItemType = .rest
|
self.workoutItemType = .rest
|
||||||
self.name = "Rest"
|
self.name = "Rest"
|
||||||
self.reps = 1
|
self.plannedReps = 1
|
||||||
self.value = rest
|
self.plannedValue = rest
|
||||||
self.metric = .time
|
self.metric = .time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,22 +13,24 @@ final class WorkoutSession: Nameable {
|
|||||||
var id = UUID()
|
var id = UUID()
|
||||||
var name = ""
|
var name = ""
|
||||||
// The Workout is what *should* happen
|
// The Workout is what *should* happen
|
||||||
var workout: Workout? {
|
var workout: Workout {
|
||||||
didSet {
|
didSet {
|
||||||
self.name = workout?.name ?? "Unknown Workout"
|
self.name = workout?.name ?? "Unknown Workout"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(start with: Workout) {
|
||||||
|
self.workout = with
|
||||||
|
}
|
||||||
|
|
||||||
// State
|
// State
|
||||||
// var isPaused: Bool
|
// var isPaused: Bool
|
||||||
// var isCancelled: Bool
|
// var isCancelled: Bool
|
||||||
// var isDeleted: Bool
|
// var isDeleted: Bool
|
||||||
// var isSynced: Bool
|
// var isSynced: Bool
|
||||||
|
|
||||||
// Time
|
|
||||||
var creationDate = Date.now
|
|
||||||
// My workout session started at:
|
// My workout session started at:
|
||||||
var startDate: Date? = nil
|
var startDate: Date = Date.now
|
||||||
// My workout session was completed at:
|
// My workout session was completed at:
|
||||||
var stopDate: Date? = nil
|
var stopDate: Date? = nil
|
||||||
// My workout session took me x seconds.
|
// My workout session took me x seconds.
|
||||||
@@ -39,17 +41,15 @@ final class WorkoutSession: Nameable {
|
|||||||
// Exercise Progress
|
// Exercise Progress
|
||||||
var currentExercise = 0
|
var currentExercise = 0
|
||||||
|
|
||||||
init () { }
|
|
||||||
|
|
||||||
func isActive() -> Bool {
|
func isActive() -> Bool {
|
||||||
return startDate != nil && stopDate == nil
|
return stopDate == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -- Workout Controls
|
// MARK: -- Workout Controls
|
||||||
func start(with workout: Workout) {
|
// func start(with workout: Workout) {
|
||||||
self.workout = workout
|
// self.workout = workout
|
||||||
startDate = Date.now
|
// startDate = Date.now
|
||||||
}
|
// }
|
||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
// TODO: Implement proper Pause
|
// TODO: Implement proper Pause
|
||||||
@@ -57,21 +57,18 @@ final class WorkoutSession: Nameable {
|
|||||||
|
|
||||||
// Call stop() to terminate the workout.
|
// Call stop() to terminate the workout.
|
||||||
func stop() {
|
func stop() {
|
||||||
guard let startDate = startDate else { return }
|
|
||||||
isCompleted = true
|
isCompleted = true
|
||||||
stopDate = Date.now
|
stopDate = Date.now
|
||||||
duration = stopDate!.timeIntervalSince(startDate)
|
duration = stopDate!.timeIntervalSince(startDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prevExercise() {
|
func prevExercise() {
|
||||||
guard workout != nil else { return }
|
|
||||||
if currentExercise > 0 {
|
if currentExercise > 0 {
|
||||||
currentExercise -= 1
|
currentExercise -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextExercise() {
|
func nextExercise() {
|
||||||
guard let workout = workout else { return }
|
|
||||||
if currentExercise < workout.getWorkoutItems().count - 1 {
|
if currentExercise < workout.getWorkoutItems().count - 1 {
|
||||||
currentExercise += 1
|
currentExercise += 1
|
||||||
}
|
}
|
||||||
@@ -79,7 +76,6 @@ final class WorkoutSession: Nameable {
|
|||||||
|
|
||||||
// MARK: -- Workout Information
|
// MARK: -- Workout Information
|
||||||
func getFormattedDuration() -> String {
|
func getFormattedDuration() -> String {
|
||||||
guard let startDate = startDate else { return "00:00:00" }
|
|
||||||
let elapsedTime = Date.now.timeIntervalSince(startDate)
|
let elapsedTime = Date.now.timeIntervalSince(startDate)
|
||||||
let formatter = DateComponentsFormatter()
|
let formatter = DateComponentsFormatter()
|
||||||
formatter.allowedUnits = [.hour, .minute, .second]
|
formatter.allowedUnits = [.hour, .minute, .second]
|
||||||
@@ -88,7 +84,6 @@ final class WorkoutSession: Nameable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTotalExerciseCount() -> Double {
|
func getTotalExerciseCount() -> Double {
|
||||||
guard let workout = workout else { return 0 }
|
|
||||||
return Double(workout.getWorkoutItems().count)
|
return Double(workout.getWorkoutItems().count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,12 +96,10 @@ final class WorkoutSession: Nameable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentExerciseName() -> String {
|
func getCurrentExerciseName() -> String {
|
||||||
guard let workout = workout else { return "Unknown Workout" }
|
|
||||||
return workout.getWorkoutItems()[Int(currentExercise)].name
|
return workout.getWorkoutItems()[Int(currentExercise)].name
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentExerciseMetric() -> String {
|
func getCurrentExerciseMetric() -> String {
|
||||||
guard let workout = workout else { return "Unknown Workout" }
|
return String(workout.getWorkoutItems()[Int(currentExercise)].plannedReps)
|
||||||
return String(workout.getWorkoutItems()[Int(currentExercise)].reps)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
WorkoutsPlus/Models/WorkoutSessionItem.swift
Normal file
25
WorkoutsPlus/Models/WorkoutSessionItem.swift
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// WorkoutSessionItem.swift
|
||||||
|
// WorkoutsPlus
|
||||||
|
//
|
||||||
|
// Created by Felix Förtsch on 15.10.24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
@Model
|
||||||
|
final class WorkoutSessionItem {
|
||||||
|
var id = UUID()
|
||||||
|
|
||||||
|
var workoutSession: WorkoutSession
|
||||||
|
|
||||||
|
var exerciseData: WorkoutItem
|
||||||
|
var actualReps: Int?
|
||||||
|
var actualValue: Double?
|
||||||
|
|
||||||
|
init(workoutSession: WorkoutSession, planned: WorkoutItem) {
|
||||||
|
self.workoutSession = workoutSession
|
||||||
|
self.exerciseData = planned
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
- TODO: Russian System (333 3333 33333, 444 4444 444444, usw -> https://www.youtube.com/watch?v=GZmtjlPTU1g) abbilden (ob System flexibel genug ist)
|
- TODO: Russian System (333 3333 33333, 444 4444 444444, usw -> https://www.youtube.com/watch?v=GZmtjlPTU1g) abbilden (ob System flexibel genug ist)
|
||||||
- Loops
|
- Loops
|
||||||
|
- Special Section: “Special Exercises” → Squat, Grease the Groove
|
||||||
|
|
||||||
## Trainingspläne
|
## Trainingspläne
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ struct Settings: View {
|
|||||||
Section(
|
Section(
|
||||||
header: Text("User"),
|
header: Text("User"),
|
||||||
footer: Text("Trainer Mode enables you to add and manage trainees.")) {
|
footer: Text("Trainer Mode enables you to add and manage trainees.")) {
|
||||||
Text(userId)
|
Text(userId)
|
||||||
TextField("name", text: $userId)
|
TextField("name", text: $userId)
|
||||||
Toggle(isOn: $isTrainerMode) {
|
Toggle(isOn: $isTrainerMode) {
|
||||||
Text("Trainer Mode")
|
Text("Trainer Mode")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Section(header: Text("Defaults")) {
|
Section(header: Text("Defaults")) {
|
||||||
StepperListItem(itemName: "Rep Count", itemValue: $reps)
|
StepperListItem(itemName: "Rep Count", itemValue: $reps)
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,18 @@ struct Settings: View {
|
|||||||
}
|
}
|
||||||
Button("Reset App", role: .destructive, action: resetApp)
|
Button("Reset App", role: .destructive, action: resetApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
NavigationLink("Credits") {
|
||||||
|
Text(
|
||||||
|
"""
|
||||||
|
# Software Components
|
||||||
|
## Duration Picker
|
||||||
|
https://github.com/mac-gallagher/DurationPicker
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Settings")
|
.navigationTitle("Settings")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,19 +12,47 @@ struct WorkoutDetail: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
|
||||||
|
@Default(\.isWorkingOut) var isWorkingOut
|
||||||
|
|
||||||
|
@Binding var activeWorkoutSession: WorkoutSession?
|
||||||
|
|
||||||
@State var workout: Workout
|
@State var workout: Workout
|
||||||
@State private var isPresentingWorkoutItemLibrarySheet = false
|
@State private var isPresentingWorkoutItemLibrarySheet = false
|
||||||
|
|
||||||
|
var numberFormatter: NumberFormatter {
|
||||||
|
let f = NumberFormatter()
|
||||||
|
f.numberStyle = .decimal
|
||||||
|
f.maximumFractionDigits = 0
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
@State var value: Decimal? = 60
|
||||||
|
@State var textFieldIsActive: Bool = false
|
||||||
|
|
||||||
|
@State var defaultRestTime: Double = 45
|
||||||
|
|
||||||
|
@State private var selectedMinutes = 0
|
||||||
|
@State private var selectedSeconds = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section(header: Text("Name & Icon")) {
|
Section(
|
||||||
NavigationLink(destination: WorkoutIconSelector(workout: workout)) {
|
content: {
|
||||||
TextField("Workout Name", text: $workout.name)
|
NavigationLink(destination: WorkoutIconSelector(workout: workout)) {
|
||||||
Image(systemName: workout.workoutIconSystemName)
|
TextField("Workout Name", text: $workout.name)
|
||||||
.scaledToFit()
|
Image(systemName: workout.workoutIconSystemName)
|
||||||
.foregroundStyle(workout.workoutIconColorName.color)
|
.scaledToFit()
|
||||||
}
|
.foregroundStyle(workout.workoutIconColorName.color)
|
||||||
}
|
}
|
||||||
|
HStack {
|
||||||
|
Text("Default Rest Time (min:s)")
|
||||||
|
Spacer()
|
||||||
|
// TODO: This is f-in horrible. But I need a break rn.
|
||||||
|
NumbersOnlyTextField(value: $defaultRestTime)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
header: { Text("Name & Icon") },
|
||||||
|
footer: { Text("Setting a Default Rest Time inserts the time between each exercise.") })
|
||||||
Section(
|
Section(
|
||||||
header: Text("Exercises"),
|
header: Text("Exercises"),
|
||||||
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.")) {
|
||||||
@@ -34,14 +62,41 @@ struct WorkoutDetail: View {
|
|||||||
.onDelete(perform: deleteWorkoutItem)
|
.onDelete(perform: deleteWorkoutItem)
|
||||||
.onMove(perform: move)
|
.onMove(perform: move)
|
||||||
.environment(\.editMode, .constant(.active)) // Always active drag mode
|
.environment(\.editMode, .constant(.active)) // Always active drag mode
|
||||||
AddItemButton(label: "Exercise", action: presentWorkoutItemLibrarySheet)
|
AddItemButton(label: "Exercise", action: presentWorkoutItemLibrarySheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("\(workout.name)")
|
.navigationTitle("\(workout.name)")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
// TODO: Add proper Sharing for workouts.
|
// TODO: Add proper Sharing for workouts.
|
||||||
ToolbarItem() { ShareLink(item: URL(filePath: "felixfoertsch.de")!) }
|
// ToolbarItem() { ShareLink(item: URL(filePath: "felixfoertsch.de")!) }
|
||||||
ToolbarItem() { EditButton() }
|
if (isWorkingOut) {
|
||||||
|
Button(action: {
|
||||||
|
isWorkingOut = false
|
||||||
|
activeWorkoutSession?.stop()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "stop.fill")
|
||||||
|
Text("Stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bold()
|
||||||
|
.fontDesign(.rounded)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Button(action: {
|
||||||
|
isWorkingOut = true
|
||||||
|
activeWorkoutSession = workout.start()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "play.fill")
|
||||||
|
Text("Start")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bold()
|
||||||
|
.fontDesign(.rounded)
|
||||||
|
.tint(.green)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isPresentingWorkoutItemLibrarySheet) {
|
.sheet(isPresented: $isPresentingWorkoutItemLibrarySheet) {
|
||||||
WorkoutItemLibrarySheet(workout: workout)
|
WorkoutItemLibrarySheet(workout: workout)
|
||||||
@@ -79,15 +134,19 @@ struct WorkoutDetail: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
@Previewable @State var activeWorkoutSession: WorkoutSession?
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
WorkoutDetail(workout: Workout.sampleData.first!)
|
WorkoutDetail(activeWorkoutSession: $activeWorkoutSession, workout: Workout.sampleData.first!)
|
||||||
.modelContainer(SampleData.shared.modelContainer)
|
.modelContainer(SampleData.shared.modelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Debug") {
|
#Preview("Debug") {
|
||||||
|
@Previewable @State var activeWorkoutSession: WorkoutSession?
|
||||||
|
|
||||||
TabView {
|
TabView {
|
||||||
WorkoutDetail(workout: Workout.sampleData.first!)
|
WorkoutDetail(activeWorkoutSession: $activeWorkoutSession, workout: Workout.sampleData.first!)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Image(systemName: "figure.run.square.stack")
|
Image(systemName: "figure.run.square.stack")
|
||||||
Text("Workouts")
|
Text("Workouts")
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import SwiftData
|
|||||||
|
|
||||||
struct WorkoutLibrary: View {
|
struct WorkoutLibrary: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
|
@Binding var activeWorkoutSession: WorkoutSession?
|
||||||
|
|
||||||
@Query(sort: \Workout.name) private var workouts: [Workout]
|
@Query(sort: \Workout.name) private var workouts: [Workout]
|
||||||
|
|
||||||
@State private var newWorkout: Workout = Workout(name: "")
|
@State private var newWorkout: Workout = Workout(name: "")
|
||||||
@@ -25,41 +27,48 @@ struct WorkoutLibrary: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
List {
|
List {
|
||||||
ForEach(filteredItems) { workout in
|
ForEach(filteredItems) { workout in
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
WorkoutDetail(workout: workout)
|
WorkoutDetail(activeWorkoutSession: $activeWorkoutSession, workout: workout)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: workout.workoutIconSystemName)
|
Button(action: {
|
||||||
.foregroundStyle(workout.workoutIconColorName.color)
|
activeWorkoutSession = workout.start()
|
||||||
Text(workout.name)
|
}) {
|
||||||
|
Image(systemName: "play.fill")
|
||||||
|
.foregroundStyle(.green)
|
||||||
}
|
}
|
||||||
|
// TODO: Decide if icon should appear here/create custom view
|
||||||
|
// Image(systemName: workout.workoutIconSystemName)
|
||||||
|
// .foregroundStyle(workout.workoutIconColorName.color)
|
||||||
|
Text(workout.name)
|
||||||
}
|
}
|
||||||
.onDelete(perform: deleteWorkout)
|
|
||||||
if filteredItems.isEmpty {
|
|
||||||
ContentUnavailableView.search(text: searchText)
|
|
||||||
}
|
|
||||||
if isAddingWorkout {
|
|
||||||
// TODO: On tap-out of the text field, it should lose focus
|
|
||||||
TextField("New Workout", text: $newWorkoutName, onCommit: {
|
|
||||||
save(workout: newWorkout)
|
|
||||||
})
|
|
||||||
.textInputAutocapitalization(.words)
|
|
||||||
.focused($isInputFieldFocused)
|
|
||||||
}
|
|
||||||
// TODO: When pressing the button again, it should check if something is being added already and if yes, save it.
|
|
||||||
AddItemButton(label: "Workout", action: addWorkout)
|
|
||||||
}
|
}
|
||||||
.searchable(text: $searchText)
|
.onDelete(perform: deleteWorkout)
|
||||||
}
|
if filteredItems.isEmpty {
|
||||||
.navigationTitle("Workouts")
|
ContentUnavailableView.search(text: searchText)
|
||||||
.toolbar {
|
|
||||||
ToolbarItem() {
|
|
||||||
EditButton()
|
|
||||||
}
|
}
|
||||||
|
if isAddingWorkout {
|
||||||
|
// TODO: On tap-out of the text field, it should lose focus
|
||||||
|
TextField("New Workout", text: $newWorkoutName, onCommit: {
|
||||||
|
save(workout: newWorkout)
|
||||||
|
})
|
||||||
|
.textInputAutocapitalization(.words)
|
||||||
|
.focused($isInputFieldFocused)
|
||||||
|
}
|
||||||
|
// TODO: When pressing the button again, it should check if something is being added already and if yes, save it.
|
||||||
|
AddItemButton(label: "Workout", action: addWorkout)
|
||||||
}
|
}
|
||||||
|
.searchable(text: $searchText)
|
||||||
|
}
|
||||||
|
.navigationTitle("Workouts")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem() {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addWorkout() {
|
private func addWorkout() {
|
||||||
@@ -93,16 +102,18 @@ struct WorkoutLibrary: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview("With Sample Data") {
|
#Preview("With Sample Data") {
|
||||||
|
@Previewable @State var activeWorkoutSession: WorkoutSession?
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
WorkoutLibrary()
|
WorkoutLibrary(activeWorkoutSession: $activeWorkoutSession)
|
||||||
}
|
}
|
||||||
.modelContainer(SampleData.shared.modelContainer)
|
.modelContainer(SampleData.shared.modelContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Empty Database") {
|
#Preview("Empty Database") {
|
||||||
|
@Previewable @State var activeWorkoutSession: WorkoutSession?
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
WorkoutLibrary()
|
WorkoutLibrary(activeWorkoutSession: $activeWorkoutSession)
|
||||||
}
|
}
|
||||||
.modelContainer(for: Workout.self, inMemory: true)
|
.modelContainer(for: Workout.self, inMemory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
// Recursive structure
|
||||||
struct WorkoutListItem: View {
|
struct WorkoutListItem: View {
|
||||||
var workout: Workout
|
var workout: Workout
|
||||||
@State var workoutItem: WorkoutItem
|
@State var workoutItem: WorkoutItem
|
||||||
@@ -17,19 +18,20 @@ struct WorkoutListItem: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: {
|
switch workoutItem.workoutItemType {
|
||||||
// workout.addExercise(from: exercise)
|
case .exercise:
|
||||||
}) {
|
HStack {
|
||||||
switch workoutItem.workoutItemType {
|
|
||||||
case .set:
|
|
||||||
SetListItem(workout: workout, set: $workoutItem)
|
|
||||||
case .exercise:
|
|
||||||
Text(workoutItem.name)
|
|
||||||
case .rest:
|
|
||||||
Text(workoutItem.name)
|
Text(workoutItem.name)
|
||||||
|
Spacer()
|
||||||
|
NumbersOnlyTextField(value: $workoutItem.plannedValue)
|
||||||
}
|
}
|
||||||
|
case .rest:
|
||||||
|
// Trivial base case for rest items
|
||||||
|
RestListItem(restTime: workoutItem.plannedValue)
|
||||||
|
case .set:
|
||||||
|
// Recursive case
|
||||||
|
SetListItem(workout: workout, set: $workoutItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ struct WorkoutListItem: View {
|
|||||||
WorkoutItem(Exercise("Squat")),
|
WorkoutItem(Exercise("Squat")),
|
||||||
WorkoutItem(Exercise("Squat"))]))
|
WorkoutItem(Exercise("Squat"))]))
|
||||||
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Push-ups")))
|
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Push-ups")))
|
||||||
|
WorkoutListItem(Workout(name: "RR"), WorkoutItem(rest: 15))
|
||||||
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Push-ups")))
|
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Push-ups")))
|
||||||
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Sprint", .distance)))
|
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Sprint", .distance)))
|
||||||
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Run", .time)))
|
WorkoutListItem(Workout(name: "RR"), WorkoutItem(Exercise("Run", .time)))
|
||||||
|
|||||||
@@ -35,5 +35,7 @@ extension WorkoutsPlusApp {
|
|||||||
Equipment.self,
|
Equipment.self,
|
||||||
Workout.self,
|
Workout.self,
|
||||||
WorkoutItem.self,
|
WorkoutItem.self,
|
||||||
WorkoutSession.self])
|
WorkoutSession.self,
|
||||||
|
WorkoutSessionItem.self,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
36
WorkoutsPlus/er-diagram.md
Normal file
36
WorkoutsPlus/er-diagram.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
Exercise 1 .. 0+ Equipment : uses
|
||||||
|
Exercise 1 -- 0+ WorkoutItem : "provides data for"
|
||||||
|
Workout 1 .. 0+ WorkoutItem : collects
|
||||||
|
Workout 1 .. 0+ ViewModel : "provides data for"
|
||||||
|
WorkoutItem 1 -- 1 WorkoutSessionItem : "provides data for"
|
||||||
|
WorkoutSession 1 -- 1+ WorkoutSessionItem : collects
|
||||||
|
ViewModel 1 -- 1+ WorkoutSession : "creates, starts"
|
||||||
|
|
||||||
|
Exercise {
|
||||||
|
string name
|
||||||
|
Equipment[] equipment
|
||||||
|
}
|
||||||
|
Workout {
|
||||||
|
string name
|
||||||
|
}
|
||||||
|
WorkoutSession {
|
||||||
|
Workout workout
|
||||||
|
time workoutSessionDuration
|
||||||
|
}
|
||||||
|
WorkoutItem {
|
||||||
|
Exercise exerciseData
|
||||||
|
int plannedReps
|
||||||
|
double plannedValue
|
||||||
|
}
|
||||||
|
WorkoutSessionItem {
|
||||||
|
WorkoutItem exerciseData
|
||||||
|
int actualReps
|
||||||
|
double actualValue
|
||||||
|
}
|
||||||
|
ViewModel {
|
||||||
|
Workout workout
|
||||||
|
WorkoutSession workoutSession
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user