diff --git a/Workouts/Models/Split.swift b/Workouts/Models/Split.swift index 780571f..e91f28c 100644 --- a/Workouts/Models/Split.swift +++ b/Workouts/Models/Split.swift @@ -48,8 +48,11 @@ extension Split: EditableEntity { fileprivate struct SplitFormView: View { @Binding var model: Split + + @State private var showingAddSheet: Bool = false + @State private var itemToEdit: SplitExerciseAssignment? = nil @State private var itemToDelete: SplitExerciseAssignment? = nil - + var body: some View { Section(header: Text("Name")) { TextField("Name", text: $model.name) @@ -62,37 +65,80 @@ fileprivate struct SplitFormView: View { } Section(header: Text("Exercises")) { - if let assignments = model.exercises, !assignments.isEmpty { - ForEach(assignments) { item in - ListItem( - title: item.exercise?.name ?? Exercise.unnamed, - subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs" - ) - .swipeActions { - Button(role: .destructive) { - itemToDelete = item - } label: { - Label("Delete", systemImage: "trash") + NavigationLink { + NavigationStack { + Form { + List { + if let assignments = model.exercises, !assignments.isEmpty { + let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exercise?.name ?? Exercise.unnamed < $1.exercise?.name ?? Exercise.unnamed : $0.order < $1.order }) + ForEach(sortedAssignments) { item in + ListItem( + title: item.exercise?.name ?? Exercise.unnamed, + subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs" + ) + .swipeActions { + Button(role: .destructive) { + itemToDelete = item + } label: { + Label("Delete", systemImage: "trash") + } + Button { + itemToEdit = item + } label: { + Label("Edit", systemImage: "pencil") + } + .tint(.indigo) + } + } + } else { + Text("No exercises added yet.") + } + } + } + .navigationTitle("Exercises") + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { showingAddSheet.toggle() }) { + Image(systemName: "plus") } } } - } else { - Text("No exercises added yet.") - } - Button(action: { - // TODO: Implement add exercise functionality - }) { - Label("Add Exercise", systemImage: "plus.circle") - } - } - .confirmationDialog("Delete Exercise?", isPresented: .constant(itemToDelete != nil), titleVisibility: .visible) { - Button("Delete", role: .destructive) { - if let item = itemToDelete { - withAnimation { - model.exercises?.removeAll { $0.id == item.id } - itemToDelete = nil + .sheet (isPresented: $showingAddSheet) { + ExercisePickerView { exercise in + itemToEdit = SplitExerciseAssignment( + order: 0, + sets: exercise.sets, + reps: exercise.reps, + weight: exercise.weight, + split: model, + exercise: exercise + ) } } + .sheet(item: $itemToEdit) { item in + SplitExerciseAssignmentAddEditView(model: item) + } + .confirmationDialog( + "Delete Exercise?", + isPresented: .constant(itemToDelete != nil), + titleVisibility: .visible + ) { + Button("Delete", role: .destructive) { + if let item = itemToDelete { + withAnimation { + model.exercises?.removeAll { $0.id == item.id } + itemToDelete = nil + } + } + } + } + + } label: { + ListItem( + text: "Exercises", + count: model.exercises?.count ?? 0 + ) } } } diff --git a/Workouts/Schema/CloudKitSyncObserver.swift b/Workouts/Schema/CloudKitSyncObserver.swift index 09b6131..92b520f 100644 --- a/Workouts/Schema/CloudKitSyncObserver.swift +++ b/Workouts/Schema/CloudKitSyncObserver.swift @@ -10,18 +10,32 @@ struct CloudKitSyncObserver: ViewModifier { content .id(refreshID) // Force view refresh when this changes .onReceive(NotificationCenter.default.publisher(for: .cloudKitDataDidChange)) { _ in - // When we receive a notification that CloudKit data changed: - // 1. Create a new UUID to force view refresh refreshID = UUID() - - // 2. Optionally, you can also manually refresh the model context - // This is sometimes needed for complex relationships Task { @MainActor in - try? modelContext.fetch(FetchDescriptor()) - // Add other model types as needed +// do { +// for entity in modelContext.container.schema.entities { +// fetchAll(of: entity.Type, from: modelContext) +// } +// } catch { +// print("ERROR: failed to fetch data on CloudKit change") +// } +// +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) +// try? modelContext.fetch(FetchDescriptor()) + // TODO: add more entities? } } } + + private func fetchAll(of type: T.Type,from modelContext: ModelContext) async throws -> [T]? { + try modelContext.fetch(FetchDescriptor()) + } } // Extension to make it easier to use the modifier diff --git a/Workouts/Utils/BadgeView.swift b/Workouts/Utils/BadgeView.swift new file mode 100644 index 0000000..7afbd1b --- /dev/null +++ b/Workouts/Utils/BadgeView.swift @@ -0,0 +1,24 @@ +// +// BadgeView.swift +// Workouts +// +// Created by rzen on 7/14/25 at 2:20 PM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI + +struct BadgeView: View { + + var badge: Badge + + var body: some View { + Text("\(badge.text)") + .bold() + .padding([.leading,.trailing], 5) + .background(badge.color) + .foregroundColor(.white) + .cornerRadius(4) + } +} diff --git a/Workouts/Utils/ListItem.swift b/Workouts/Utils/ListItem.swift index e2bbceb..1450f59 100644 --- a/Workouts/Utils/ListItem.swift +++ b/Workouts/Utils/ListItem.swift @@ -10,7 +10,8 @@ import SwiftUI struct ListItem: View { - var title: String + var title: String? + var text: String? var subtitle: String? var count: Int? var badges: [Badge]? = [] @@ -18,17 +19,18 @@ struct ListItem: View { var body: some View { HStack { VStack (alignment: .leading) { - Text("\(title)") - .font(.headline) + if let title = title { + Text("\(title)") + .font(.headline) + } else if let text = text { + Text("\(text)") + } else { + Text("Untitled") + } HStack (alignment: .bottom) { if let badges = badges { ForEach (badges, id: \.self) { badge in - Text("\(badge.text)") - .bold() - .padding([.leading,.trailing], 5) - .background(badge.color) - .foregroundColor(.white) - .cornerRadius(4) + BadgeView(badge: badge) } } if let subtitle = subtitle { @@ -44,6 +46,7 @@ struct ListItem: View { .foregroundColor(.gray) } } + .frame(height: 40) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } diff --git a/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift b/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift new file mode 100644 index 0000000..5ea3dc9 --- /dev/null +++ b/Workouts/Views/Settings/SplitExerciseAssignmentAddEditView.swift @@ -0,0 +1,59 @@ +// +// SplitExerciseAssignment.swift +// Workouts +// +// Created by rzen on 7/15/25 at 7:12 AM. +// +// Copyright 2025 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI + +struct SplitExerciseAssignmentAddEditView: View { + @Environment(\.modelContext) private var modelContext + @Environment(\.dismiss) private var dismiss + + @State var model: SplitExerciseAssignment + + var body: some View { + NavigationStack { + Form { + Section(header: Text("Sets/Reps")) { + Stepper("Sets: \(model.sets)", value: $model.sets, in: 1...10) + Stepper("Reps: \(model.reps)", value: $model.reps, in: 1...50) + } + + // Weight section + Section(header: Text("Weight")) { + HStack { + VStack(alignment: .center) { + Text("\(model.weight) lbs") + .font(.headline) + } + Spacer() + VStack(alignment: .trailing) { + Stepper("±1", value: $model.weight, in: 0...1000) + Stepper("±5", value: $model.weight, in: 0...1000, step: 5) + } + .frame(width: 130) + } + } + } + .navigationTitle("\(model.exercise?.name ?? Exercise.unnamed)") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + try? modelContext.save() + dismiss() + } + } + } + } + } +} diff --git a/Workouts/Views/Workouts/ExercisePickerView.swift b/Workouts/Views/Workouts/ExercisePickerView.swift index 648fe57..0f372d0 100644 --- a/Workouts/Views/Workouts/ExercisePickerView.swift +++ b/Workouts/Views/Workouts/ExercisePickerView.swift @@ -14,7 +14,9 @@ struct ExercisePickerView: View { @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss - @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise] + @Query(sort: [SortDescriptor(\ExerciseType.name)]) private var exerciseTypes: [ExerciseType] + +// @Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise] var onExerciseSelected: (Exercise) -> Void @@ -22,18 +24,24 @@ struct ExercisePickerView: View { NavigationStack { VStack { Form { - Section (header: Text("This Split")) { - List { - ForEach(exercises) { exercise in - Button(action: { - onExerciseSelected(exercise) - dismiss() - }) { - ListItem(title: exercise.name) + ForEach (exerciseTypes) { exerciseType in + if let exercises = exerciseType.exercises, !exercises.isEmpty { + let sortedExercises = exercises.sorted(by: { $0.name < $1.name }) + Section (header: Text("\(exerciseType.name)")) { + List { + ForEach(sortedExercises) { exercise in + Button(action: { + onExerciseSelected(exercise) + dismiss() + }) { + ListItem(text: exercise.name) + } + .buttonStyle(.plain) + } } - .buttonStyle(.plain) } } + } } } diff --git a/Workouts/Views/Workouts/WorkoutEditView.swift b/Workouts/Views/Workouts/WorkoutEditView.swift index 996d2ea..195e622 100644 --- a/Workouts/Views/Workouts/WorkoutEditView.swift +++ b/Workouts/Views/Workouts/WorkoutEditView.swift @@ -15,14 +15,14 @@ struct WorkoutEditView: View { @Environment(\.dismiss) private var dismiss @State var workout: Workout - @State var endDateHidden: Bool = false + @State var workoutEndDate: Date = Date() var body: some View { NavigationStack { Form { - Section (header: Text("Split")) { - Text("\(workout.split?.name ?? Split.unnamed)") - } +// Section (header: Text("Split")) { +// Text("\(workout.split?.name ?? Split.unnamed)") +// } Section (header: Text("Start/End")) { DatePicker("Started", selection: $workout.start) @@ -31,38 +31,35 @@ struct WorkoutEditView: View { set: { newValue in withAnimation { if newValue { - workout.end = Date() - endDateHidden = false + workoutEndDate = Date() + workout.end = workoutEndDate } else { workout.end = nil - endDateHidden = true } } } )) - if !endDateHidden { - DatePicker("Ended", selection: $workout.start) + if workout.end != nil { + DatePicker("Ended", selection: $workoutEndDate) } } - .onAppear { - endDateHidden = workout.end == nil - } - Section (header: Text("Workout Log")) { - if let workoutLogs = workout.logs { - List { - ForEach (workoutLogs) { log in - ListItem( - title: log.exercise?.name ?? Exercise.unnamed, - subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs" - ) - } - } - } else { - Text("No workout logs yet") - } - } +// Section (header: Text("Workout Log")) { +// if let workoutLogs = workout.logs { +// List { +// ForEach (workoutLogs) { log in +// ListItem( +// title: log.exercise?.name ?? Exercise.unnamed, +// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs" +// ) +// } +// } +// } else { +// Text("No workout logs yet") +// } +// } } + .navigationTitle("\(workout.split?.name ?? Split.unnamed) Split") .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { diff --git a/Workouts/Views/Workouts/WorkoutLogView.swift b/Workouts/Views/Workouts/WorkoutLogView.swift index 0de9e77..6c04341 100644 --- a/Workouts/Views/Workouts/WorkoutLogView.swift +++ b/Workouts/Views/Workouts/WorkoutLogView.swift @@ -20,7 +20,9 @@ struct WorkoutLogView: View { var sortedWorkoutLogs: [WorkoutLog] { if let logs = workout.logs { - logs.sorted(by: { $0.exercise!.name < $1.exercise!.name }) + logs.sorted(by: { + $0.completed == $1.completed ? $0.exercise!.name < $1.exercise!.name : !$0.completed + }) } else { [] } @@ -43,16 +45,20 @@ struct WorkoutLogView: View { .swipeActions(edge: .leading, allowsFullSwipe: false) { if (log.completed) { Button { - log.completed = false - try? modelContext.save() + withAnimation { + log.completed = false + try? modelContext.save() + } } label: { Label("Complete", systemImage: "circle.fill") } .tint(.green) } else { Button { - log.completed = true - try? modelContext.save() + withAnimation { + log.completed = true + try? modelContext.save() + } } label: { Label("Reset", systemImage: "checkmark.circle.fill") } @@ -76,7 +82,7 @@ struct WorkoutLogView: View { } } } - .navigationTitle("\(workout.split?.name ?? Split.unnamed)") + .navigationTitle("\(workout.split?.name ?? Split.unnamed) Split") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button(action: { showingAddSheet.toggle() }) {