This commit is contained in:
2025-07-15 16:33:55 -04:00
parent 39fd45e03f
commit 48bbbbf692
8 changed files with 242 additions and 85 deletions

View File

@ -48,6 +48,9 @@ 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 {
@ -62,11 +65,16 @@ fileprivate struct SplitFormView: View {
}
Section(header: Text("Exercises")) {
NavigationLink {
NavigationStack {
Form {
List {
if let assignments = model.exercises, !assignments.isEmpty {
ForEach(assignments) { item in
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"
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs"
)
.swipeActions {
Button(role: .destructive) {
@ -74,18 +82,48 @@ fileprivate struct SplitFormView: View {
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
} 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) {
.navigationTitle("Exercises")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.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 {
@ -95,5 +133,13 @@ fileprivate struct SplitFormView: View {
}
}
}
} label: {
ListItem(
text: "Exercises",
count: model.exercises?.count ?? 0
)
}
}
}
}

View File

@ -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<Exercise>())
// Add other model types as needed
// do {
// for entity in modelContext.container.schema.entities {
// fetchAll<entity.Type>(of: entity.Type, from: modelContext)
// }
// } catch {
// print("ERROR: failed to fetch data on CloudKit change")
// }
//
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<ExerciseType>())
// try? modelContext.fetch(FetchDescriptor<Muscle>())
// try? modelContext.fetch(FetchDescriptor<MuscleGroup>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// try? modelContext.fetch(FetchDescriptor<Exercise>())
// TODO: add more entities?
}
}
}
private func fetchAll<T: PersistentModel>(of type: T.Type,from modelContext: ModelContext) async throws -> [T]? {
try modelContext.fetch(FetchDescriptor<T>())
}
}
// Extension to make it easier to use the modifier

View File

@ -0,0 +1,24 @@
//
// BadgeView.swift
// Workouts
//
// Created by rzen on 7/14/25 at 2:20PM.
//
// 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)
}
}

View File

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

View File

@ -0,0 +1,59 @@
//
// SplitExerciseAssignment.swift
// Workouts
//
// Created by rzen on 7/15/25 at 7:12AM.
//
// 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()
}
}
}
}
}
}

View File

@ -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,20 +24,26 @@ struct ExercisePickerView: View {
NavigationStack {
VStack {
Form {
Section (header: Text("This Split")) {
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(exercises) { exercise in
ForEach(sortedExercises) { exercise in
Button(action: {
onExerciseSelected(exercise)
dismiss()
}) {
ListItem(title: exercise.name)
ListItem(text: exercise.name)
}
.buttonStyle(.plain)
}
}
}
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {

View File

@ -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") {

View File

@ -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 {
withAnimation {
log.completed = false
try? modelContext.save()
}
} label: {
Label("Complete", systemImage: "circle.fill")
}
.tint(.green)
} else {
Button {
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() }) {