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 @@
+
+
+