From 8b6250e4d65933a9336a547411b7c20886c7bb99 Mon Sep 17 00:00:00 2001 From: rzen Date: Mon, 19 Jan 2026 16:10:37 -0500 Subject: [PATCH] Refactor UI: move Splits to Settings, redesign ExerciseView Schema & Models: - Add notes, loadType, duration fields to WorkoutLog - Align Watch schema with iOS (use duration Date instead of separate mins/secs) - Add duration helper properties to Exercise and WorkoutLog UI Changes: - Remove Splits and Settings tabs, single Workout Logs view - Add gear button in nav bar to access Settings as sheet - Move Splits section into Settings view with inline list - Redesign ExerciseView with read-only Plan/Notes tiles and Edit buttons - Add PlanEditView and NotesEditView with Cancel/Save buttons - Auto-dismiss ExerciseView when completing last set - Navigate to ExerciseView when adding new exercise Data Flow: - Plan edits sync to both WorkoutLog and corresponding Exercise - Changes propagate up navigation chain via CoreData --- .claude/settings.local.json | 5 +- Workouts Watch App/Models/Exercise.swift | 23 ++ Workouts Watch App/Models/WorkoutLog.swift | 46 +++- .../Workouts.xcdatamodel/contents | 3 + Workouts/ContentView.swift | 17 +- Workouts/Models/Exercise.swift | 23 ++ Workouts/Models/WorkoutLog.swift | 31 +++ Workouts/Views/Settings/SettingsView.swift | 69 ++++++ Workouts/Views/WorkoutLogs/ExerciseView.swift | 213 +++++++++++------- .../Views/WorkoutLogs/NotesEditView.swift | 52 +++++ Workouts/Views/WorkoutLogs/PlanEditView.swift | 167 ++++++++++++++ .../WorkoutLogs/WorkoutLogListView.swift | 35 ++- .../Views/WorkoutLogs/WorkoutLogsView.swift | 11 + .../Workouts.xcdatamodel/contents | 3 + 14 files changed, 592 insertions(+), 106 deletions(-) create mode 100644 Workouts/Views/WorkoutLogs/NotesEditView.swift create mode 100644 Workouts/Views/WorkoutLogs/PlanEditView.swift diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 15546fb..dcfd885 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,10 @@ "Bash(xcrun simctl install:*)", "Bash(xcrun simctl launch:*)", "Bash(xcrun simctl get_app_container:*)", - "Bash(log show:*)" + "Bash(log show:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)" ], "deny": [], "ask": [] diff --git a/Workouts Watch App/Models/Exercise.swift b/Workouts Watch App/Models/Exercise.swift index f923813..be69e0f 100644 --- a/Workouts Watch App/Models/Exercise.swift +++ b/Workouts Watch App/Models/Exercise.swift @@ -21,6 +21,29 @@ public class Exercise: NSManagedObject, Identifiable { get { LoadType(rawValue: Int(loadType)) ?? .weight } set { loadType = Int32(newValue.rawValue) } } + + // Duration helpers for minutes/seconds conversion + var durationMinutes: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) / 60 + } + set { + let seconds = durationSeconds + duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds)) + } + } + + var durationSeconds: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) % 60 + } + set { + let minutes = durationMinutes + duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue)) + } + } } // MARK: - Fetch Request diff --git a/Workouts Watch App/Models/WorkoutLog.swift b/Workouts Watch App/Models/WorkoutLog.swift index e5ef0b3..c5936fa 100644 --- a/Workouts Watch App/Models/WorkoutLog.swift +++ b/Workouts Watch App/Models/WorkoutLog.swift @@ -7,23 +7,59 @@ public class WorkoutLog: NSManagedObject, Identifiable { @NSManaged public var sets: Int32 @NSManaged public var reps: Int32 @NSManaged public var weight: Int32 - @NSManaged private var statusRaw: String? @NSManaged public var order: Int32 @NSManaged public var exerciseName: String @NSManaged public var currentStateIndex: Int32 @NSManaged public var elapsedSeconds: Int32 @NSManaged public var completed: Bool + @NSManaged public var loadType: Int32 + @NSManaged public var duration: Date? + @NSManaged public var notes: String? @NSManaged public var workout: Workout? public var id: NSManagedObjectID { objectID } - var status: WorkoutStatus? { + var status: WorkoutStatus { get { - guard let raw = statusRaw else { return nil } - return WorkoutStatus(rawValue: raw) + willAccessValue(forKey: "status") + let raw = primitiveValue(forKey: "status") as? String ?? "notStarted" + didAccessValue(forKey: "status") + return WorkoutStatus(rawValue: raw) ?? .notStarted + } + set { + willChangeValue(forKey: "status") + setPrimitiveValue(newValue.rawValue, forKey: "status") + didChangeValue(forKey: "status") + } + } + + var loadTypeEnum: LoadType { + get { LoadType(rawValue: Int(loadType)) ?? .weight } + set { loadType = Int32(newValue.rawValue) } + } + + // Duration helpers for minutes/seconds conversion + var durationMinutes: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) / 60 + } + set { + let seconds = durationSeconds + duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds)) + } + } + + var durationSeconds: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) % 60 + } + set { + let minutes = durationMinutes + duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue)) } - set { statusRaw = newValue?.rawValue } } } diff --git a/Workouts Watch App/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents b/Workouts Watch App/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents index 335f8d6..38606fb 100644 --- a/Workouts Watch App/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents +++ b/Workouts Watch App/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents @@ -31,8 +31,11 @@ + + + diff --git a/Workouts/ContentView.swift b/Workouts/ContentView.swift index 7dfd824..94b99b1 100644 --- a/Workouts/ContentView.swift +++ b/Workouts/ContentView.swift @@ -15,22 +15,7 @@ struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext var body: some View { - TabView { - WorkoutLogsView() - .tabItem { - Label("Workout Logs", systemImage: "list.bullet.clipboard") - } - - SplitsView() - .tabItem { - Label("Splits", systemImage: "dumbbell.fill") - } - - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } - } + WorkoutLogsView() } } diff --git a/Workouts/Models/Exercise.swift b/Workouts/Models/Exercise.swift index f923813..be69e0f 100644 --- a/Workouts/Models/Exercise.swift +++ b/Workouts/Models/Exercise.swift @@ -21,6 +21,29 @@ public class Exercise: NSManagedObject, Identifiable { get { LoadType(rawValue: Int(loadType)) ?? .weight } set { loadType = Int32(newValue.rawValue) } } + + // Duration helpers for minutes/seconds conversion + var durationMinutes: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) / 60 + } + set { + let seconds = durationSeconds + duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds)) + } + } + + var durationSeconds: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) % 60 + } + set { + let minutes = durationMinutes + duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue)) + } + } } // MARK: - Fetch Request diff --git a/Workouts/Models/WorkoutLog.swift b/Workouts/Models/WorkoutLog.swift index 8ecb436..c5936fa 100644 --- a/Workouts/Models/WorkoutLog.swift +++ b/Workouts/Models/WorkoutLog.swift @@ -12,6 +12,9 @@ public class WorkoutLog: NSManagedObject, Identifiable { @NSManaged public var currentStateIndex: Int32 @NSManaged public var elapsedSeconds: Int32 @NSManaged public var completed: Bool + @NSManaged public var loadType: Int32 + @NSManaged public var duration: Date? + @NSManaged public var notes: String? @NSManaged public var workout: Workout? @@ -30,6 +33,34 @@ public class WorkoutLog: NSManagedObject, Identifiable { didChangeValue(forKey: "status") } } + + var loadTypeEnum: LoadType { + get { LoadType(rawValue: Int(loadType)) ?? .weight } + set { loadType = Int32(newValue.rawValue) } + } + + // Duration helpers for minutes/seconds conversion + var durationMinutes: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) / 60 + } + set { + let seconds = durationSeconds + duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds)) + } + } + + var durationSeconds: Int { + get { + guard let duration = duration else { return 0 } + return Int(duration.timeIntervalSince1970) % 60 + } + set { + let minutes = durationMinutes + duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue)) + } + } } // MARK: - Fetch Request diff --git a/Workouts/Views/Settings/SettingsView.swift b/Workouts/Views/Settings/SettingsView.swift index dc1ef26..99edb78 100644 --- a/Workouts/Views/Settings/SettingsView.swift +++ b/Workouts/Views/Settings/SettingsView.swift @@ -6,17 +6,82 @@ // import SwiftUI +import CoreData import IndieAbout struct SettingsView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [ + NSSortDescriptor(keyPath: \Split.order, ascending: true), + NSSortDescriptor(keyPath: \Split.name, ascending: true) + ], + animation: .default + ) + private var splits: FetchedResults + + @State private var showingAddSplitSheet = false + var body: some View { NavigationStack { Form { + // MARK: - Splits Section + Section(header: Text("Splits")) { + if splits.isEmpty { + HStack { + Spacer() + VStack(spacing: 8) { + Image(systemName: "dumbbell.fill") + .font(.largeTitle) + .foregroundColor(.secondary) + Text("No Splits Yet") + .font(.headline) + .foregroundColor(.secondary) + Text("Create a split to organize your workout routine.") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding(.vertical) + Spacer() + } + } else { + ForEach(splits, id: \.objectID) { split in + NavigationLink { + SplitDetailView(split: split) + } label: { + HStack { + Image(systemName: split.systemImage) + .foregroundColor(Color.color(from: split.color)) + .frame(width: 24) + Text(split.name) + Spacer() + Text("\(split.exercisesArray.count)") + .foregroundColor(.secondary) + } + } + } + } + + Button { + showingAddSplitSheet = true + } label: { + HStack { + Image(systemName: "plus.circle.fill") + .foregroundColor(.accentColor) + Text("Add Split") + } + } + } + + // MARK: - Account Section Section(header: Text("Account")) { Text("Settings coming soon") .foregroundColor(.secondary) } + // MARK: - About Section Section { IndieAbout(configuration: AppInfoConfiguration( documents: [ @@ -28,10 +93,14 @@ struct SettingsView: View { } } .navigationTitle("Settings") + .sheet(isPresented: $showingAddSplitSheet) { + SplitAddEditView(split: nil) + } } } } #Preview { SettingsView() + .environment(\.managedObjectContext, PersistenceController.preview.viewContext) } diff --git a/Workouts/Views/WorkoutLogs/ExerciseView.swift b/Workouts/Views/WorkoutLogs/ExerciseView.swift index f939542..78c92ce 100644 --- a/Workouts/Views/WorkoutLogs/ExerciseView.swift +++ b/Workouts/Views/WorkoutLogs/ExerciseView.swift @@ -17,46 +17,19 @@ struct ExerciseView: View { @ObservedObject var workoutLog: WorkoutLog - var allLogs: [WorkoutLog] - var currentIndex: Int = 0 - @State private var progress: Int = 0 - @State private var navigateTo: WorkoutLog? = nil + @State private var showingPlanEdit = false + @State private var showingNotesEdit = false let notStartedColor = Color.white let completedColor = Color.green var body: some View { Form { - Section(header: Text("Navigation")) { - HStack { - Button(action: navigateToPrevious) { - HStack { - Image(systemName: "chevron.left") - Text("Previous") - } - } - .disabled(currentIndex <= 0) - - Spacer() - Text("\(currentIndex + 1) of \(allLogs.count)") - .foregroundColor(.secondary) - Spacer() - - Button(action: navigateToNext) { - HStack { - Text("Next") - Image(systemName: "chevron.right") - } - } - .disabled(currentIndex >= allLogs.count - 1) - } - .padding(.vertical, 8) - } - + // MARK: - Progress Section Section(header: Text("Progress")) { - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: Int(workoutLog.sets)), spacing: 2) { - ForEach(1...Int(workoutLog.sets), id: \.self) { index in + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: Int(workoutLog.sets)), spacing: 4) { + ForEach(1...max(1, Int(workoutLog.sets)), id: \.self) { index in ZStack { let completed = index <= progress let color = completed ? completedColor : notStartedColor @@ -71,95 +44,181 @@ struct ExerciseView: View { .aspectRatio(0.618, contentMode: .fit) .shadow(radius: 2) Text("\(index)") + .font(.title) + .fontWeight(.bold) .foregroundColor(.primary) .colorInvert() } .onTapGesture { - if progress == index { - progress = 0 - } else { - progress = index + let totalSets = Int(workoutLog.sets) + let isLastTile = index == totalSets + let wasAlreadyAtThisProgress = progress == index + + withAnimation(.easeInOut(duration: 0.2)) { + if wasAlreadyAtThisProgress { + progress = 0 + } else { + progress = index + } } + updateLogStatus() + + // If tapping the last tile to complete, go back to list + if isLastTile && !wasAlreadyAtThisProgress { + dismiss() + } } } } } - Section(header: Text("Plan")) { - Stepper("\(workoutLog.sets) sets", value: Binding( - get: { Int(workoutLog.sets) }, - set: { workoutLog.sets = Int32($0) } - ), in: 1...10) - .font(.title) - - Stepper("\(workoutLog.reps) reps", value: Binding( - get: { Int(workoutLog.reps) }, - set: { workoutLog.reps = Int32($0) } - ), in: 1...25) - .font(.title) - + // MARK: - Plan Section (Read-only with Edit button) + Section { + PlanTilesView(workoutLog: workoutLog) + } header: { HStack { - Text("\(workoutLog.weight) lbs") - VStack(alignment: .trailing) { - Stepper("", value: Binding( - get: { Int(workoutLog.weight) }, - set: { workoutLog.weight = Int32($0) } - ), in: 1...500) - Stepper("", value: Binding( - get: { Int(workoutLog.weight) }, - set: { workoutLog.weight = Int32($0) } - ), in: 1...500, step: 5) + Text("Plan") + Spacer() + Button("Edit") { + showingPlanEdit = true } + .font(.subheadline) + .textCase(.none) } - .font(.title) } + // MARK: - Notes Section (Read-only with Edit button) + Section { + if let notes = workoutLog.notes, !notes.isEmpty { + Text(notes) + .foregroundColor(.primary) + } else { + Text("No notes") + .foregroundColor(.secondary) + .italic() + } + } header: { + HStack { + Text("Notes") + Spacer() + Button("Edit") { + showingNotesEdit = true + } + .font(.subheadline) + .textCase(.none) + } + } + + // MARK: - Progress Tracking Chart Section(header: Text("Progress Tracking")) { WeightProgressionChartView(exerciseName: workoutLog.exerciseName) } } .navigationTitle(workoutLog.exerciseName) - .navigationDestination(item: $navigateTo) { nextLog in - ExerciseView( - workoutLog: nextLog, - allLogs: allLogs, - currentIndex: allLogs.firstIndex(where: { $0.objectID == nextLog.objectID }) ?? 0 - ) + .sheet(isPresented: $showingPlanEdit) { + PlanEditView(workoutLog: workoutLog) + } + .sheet(isPresented: $showingNotesEdit) { + NotesEditView(workoutLog: workoutLog) } .onAppear { progress = Int(workoutLog.currentStateIndex) } - .onDisappear { - saveChanges() - } } private func updateLogStatus() { workoutLog.currentStateIndex = Int32(progress) if progress >= Int(workoutLog.sets) { workoutLog.status = .completed + workoutLog.completed = true } else if progress > 0 { workoutLog.status = .inProgress + workoutLog.completed = false } else { workoutLog.status = .notStarted + workoutLog.completed = false } + updateWorkoutStatus() saveChanges() } + private func updateWorkoutStatus() { + guard let workout = workoutLog.workout else { return } + let logs = workout.logsArray + let allCompleted = logs.allSatisfy { $0.status == .completed } + let anyInProgress = logs.contains { $0.status == .inProgress } + let allNotStarted = logs.allSatisfy { $0.status == .notStarted } + + if allCompleted { + workout.status = .completed + workout.end = Date() + } else if anyInProgress || !allNotStarted { + workout.status = .inProgress + } else { + workout.status = .notStarted + } + } + private func saveChanges() { try? viewContext.save() } +} - private func navigateToPrevious() { - guard currentIndex > 0 else { return } - let previousIndex = currentIndex - 1 - navigateTo = allLogs[previousIndex] +// MARK: - Plan Tiles View + +struct PlanTilesView: View { + @ObservedObject var workoutLog: WorkoutLog + + var body: some View { + if workoutLog.loadTypeEnum == .duration { + // Duration layout: Sets | Duration + HStack(spacing: 0) { + PlanTile(label: "Sets", value: "\(workoutLog.sets)") + PlanTile(label: "Duration", value: formattedDuration) + } + } else { + // Weight layout: Sets | Reps | Weight + HStack(spacing: 0) { + PlanTile(label: "Sets", value: "\(workoutLog.sets)") + PlanTile(label: "Reps", value: "\(workoutLog.reps)") + PlanTile(label: "Weight", value: "\(workoutLog.weight) lbs") + } + } } - private func navigateToNext() { - guard currentIndex < allLogs.count - 1 else { return } - let nextIndex = currentIndex + 1 - navigateTo = allLogs[nextIndex] + private var formattedDuration: String { + let mins = workoutLog.durationMinutes + let secs = workoutLog.durationSeconds + if mins > 0 && secs > 0 { + return "\(mins)m \(secs)s" + } else if mins > 0 { + return "\(mins) min" + } else if secs > 0 { + return "\(secs) sec" + } else { + return "0 sec" + } + } +} + +struct PlanTile: View { + let label: String + let value: String + + var body: some View { + VStack(spacing: 4) { + Text(label) + .font(.caption) + .foregroundColor(.secondary) + Text(value) + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color(.systemGray6)) + .cornerRadius(8) } } diff --git a/Workouts/Views/WorkoutLogs/NotesEditView.swift b/Workouts/Views/WorkoutLogs/NotesEditView.swift new file mode 100644 index 0000000..8582a16 --- /dev/null +++ b/Workouts/Views/WorkoutLogs/NotesEditView.swift @@ -0,0 +1,52 @@ +// +// NotesEditView.swift +// Workouts +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI +import CoreData + +struct NotesEditView: View { + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.dismiss) private var dismiss + + @ObservedObject var workoutLog: WorkoutLog + + @State private var notesText: String = "" + + var body: some View { + NavigationStack { + Form { + Section { + TextEditor(text: $notesText) + .frame(minHeight: 200) + } + } + .navigationTitle("Edit Notes") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + saveChanges() + dismiss() + } + } + } + .onAppear { + notesText = workoutLog.notes ?? "" + } + } + } + + private func saveChanges() { + workoutLog.notes = notesText + try? viewContext.save() + } +} diff --git a/Workouts/Views/WorkoutLogs/PlanEditView.swift b/Workouts/Views/WorkoutLogs/PlanEditView.swift new file mode 100644 index 0000000..18eeb7f --- /dev/null +++ b/Workouts/Views/WorkoutLogs/PlanEditView.swift @@ -0,0 +1,167 @@ +// +// PlanEditView.swift +// Workouts +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI +import CoreData + +struct PlanEditView: View { + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.dismiss) private var dismiss + + @ObservedObject var workoutLog: WorkoutLog + + @State private var sets: Int = 3 + @State private var reps: Int = 12 + @State private var weight: Int = 0 + @State private var durationMinutes: Int = 0 + @State private var durationSeconds: Int = 0 + @State private var selectedLoadType: LoadType = .weight + + // Find the corresponding exercise in the split for syncing changes + private var correspondingExercise: Exercise? { + workoutLog.workout?.split?.exercisesArray.first { $0.name == workoutLog.exerciseName } + } + + var body: some View { + NavigationStack { + Form { + // Sets and Reps side by side + Section { + HStack(spacing: 20) { + VStack { + Text("Sets") + .font(.headline) + .foregroundColor(.secondary) + Picker("Sets", selection: $sets) { + ForEach(1...7, id: \.self) { num in + Text("\(num)").tag(num) + } + } + .pickerStyle(.wheel) + .frame(height: 120) + } + .frame(maxWidth: .infinity) + + VStack { + Text("Reps") + .font(.headline) + .foregroundColor(.secondary) + Picker("Reps", selection: $reps) { + ForEach(1...40, id: \.self) { num in + Text("\(num)").tag(num) + } + } + .pickerStyle(.wheel) + .frame(height: 120) + } + .frame(maxWidth: .infinity) + } + } + + // Load Type Picker + Section { + Picker("Load Type", selection: $selectedLoadType) { + Text("Weight").tag(LoadType.weight) + Text("Time").tag(LoadType.duration) + } + .pickerStyle(.segmented) + } + + // Weight or Time picker based on load type + Section { + if selectedLoadType == .weight { + VStack { + Text("Weight") + .font(.headline) + .foregroundColor(.secondary) + Picker("Weight", selection: $weight) { + ForEach(0...300, id: \.self) { num in + Text("\(num) lbs").tag(num) + } + } + .pickerStyle(.wheel) + .frame(height: 150) + } + } else { + HStack(spacing: 20) { + VStack { + Text("Mins") + .font(.headline) + .foregroundColor(.secondary) + Picker("Minutes", selection: $durationMinutes) { + ForEach(0...60, id: \.self) { num in + Text("\(num)").tag(num) + } + } + .pickerStyle(.wheel) + .frame(height: 120) + } + .frame(maxWidth: .infinity) + + VStack { + Text("Secs") + .font(.headline) + .foregroundColor(.secondary) + Picker("Seconds", selection: $durationSeconds) { + ForEach(0...59, id: \.self) { num in + Text("\(num)").tag(num) + } + } + .pickerStyle(.wheel) + .frame(height: 120) + } + .frame(maxWidth: .infinity) + } + } + } + } + .navigationTitle("Edit Plan") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + saveChanges() + dismiss() + } + } + } + .onAppear { + sets = Int(workoutLog.sets) + reps = Int(workoutLog.reps) + weight = Int(workoutLog.weight) + durationMinutes = workoutLog.durationMinutes + durationSeconds = workoutLog.durationSeconds + selectedLoadType = workoutLog.loadTypeEnum + } + } + } + + private func saveChanges() { + workoutLog.sets = Int32(sets) + workoutLog.reps = Int32(reps) + workoutLog.weight = Int32(weight) + workoutLog.durationMinutes = durationMinutes + workoutLog.durationSeconds = durationSeconds + workoutLog.loadTypeEnum = selectedLoadType + + // Sync to corresponding exercise + if let exercise = correspondingExercise { + exercise.sets = workoutLog.sets + exercise.reps = workoutLog.reps + exercise.weight = workoutLog.weight + exercise.loadType = workoutLog.loadType + exercise.duration = workoutLog.duration + } + + try? viewContext.save() + } +} diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift index 5ef760a..cd77383 100644 --- a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift +++ b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift @@ -17,6 +17,7 @@ struct WorkoutLogListView: View { @State private var showingAddSheet = false @State private var itemToDelete: WorkoutLog? = nil + @State private var newlyAddedLog: WorkoutLog? = nil var sortedWorkoutLogs: [WorkoutLog] { workout.logsArray @@ -40,20 +41,16 @@ struct WorkoutLogListView: View { } else { Form { Section(header: Text("\(workout.label)")) { - ForEach(Array(sortedWorkoutLogs.enumerated()), id: \.element.objectID) { index, log in + ForEach(sortedWorkoutLogs, id: \.objectID) { log in let workoutLogStatus = log.status.checkboxStatus NavigationLink { - ExerciseView( - workoutLog: log, - allLogs: sortedWorkoutLogs, - currentIndex: index - ) + ExerciseView(workoutLog: log) } label: { CheckboxListItem( status: workoutLogStatus, title: log.exerciseName, - subtitle: "\(log.sets) × \(log.reps) reps × \(log.weight) lbs" + subtitle: subtitleForLog(log) ) { cycleStatus(for: log) } @@ -80,6 +77,9 @@ struct WorkoutLogListView: View { } } } + .navigationDestination(item: $newlyAddedLog) { log in + ExerciseView(workoutLog: log) + } .navigationTitle("\(workout.split?.name ?? Split.unnamed)") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -182,10 +182,31 @@ struct WorkoutLogListView: View { log.sets = exercise.sets log.reps = exercise.reps log.weight = exercise.weight + log.loadType = exercise.loadType + log.duration = exercise.duration log.status = .notStarted log.workout = workout try? viewContext.save() + + // Navigate to the new exercise view + newlyAddedLog = log + } + + private func subtitleForLog(_ log: WorkoutLog) -> String { + if log.loadTypeEnum == .duration { + let mins = log.durationMinutes + let secs = log.durationSeconds + if mins > 0 && secs > 0 { + return "\(log.sets) × \(mins)m \(secs)s" + } else if mins > 0 { + return "\(log.sets) × \(mins) min" + } else { + return "\(log.sets) × \(secs) sec" + } + } else { + return "\(log.sets) × \(log.reps) reps × \(log.weight) lbs" + } } } diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift index 05db85c..b53f7df 100644 --- a/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift +++ b/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift @@ -20,6 +20,7 @@ struct WorkoutLogsView: View { private var workouts: FetchedResults @State private var showingSplitPicker = false + @State private var showingSettings = false @State private var itemToDelete: Workout? = nil var body: some View { @@ -55,12 +56,22 @@ struct WorkoutLogsView: View { } .navigationTitle("Workout Logs") .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + showingSettings = true + } label: { + Image(systemName: "gearshape.2") + } + } ToolbarItem(placement: .navigationBarTrailing) { Button("Start New") { showingSplitPicker.toggle() } } } + .sheet(isPresented: $showingSettings) { + SettingsView() + } .sheet(isPresented: $showingSplitPicker) { SplitPickerSheet() } diff --git a/Workouts/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents b/Workouts/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents index 335f8d6..38606fb 100644 --- a/Workouts/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents +++ b/Workouts/Workouts.xcdatamodeld/Workouts.xcdatamodel/contents @@ -31,8 +31,11 @@ + + +