This commit is contained in:
2025-07-13 21:54:09 -04:00
parent 0545f5dbc7
commit bdaa406876
33 changed files with 984 additions and 714 deletions

View File

@ -1,48 +0,0 @@
//
// ExerciseTypeAddEditView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 11:33AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct ExerciseTypeAddEditView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Bindable var model: ExerciseType
var body: some View {
NavigationStack {
Form {
Section (header: Text("Nname")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
.padding(.vertical, 4)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
try? modelContext.save()
dismiss()
}
}
}
}
}
}

View File

@ -1,75 +0,0 @@
//
// ExerciseTypeListView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 11:27AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct ExerciseTypeListView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var items: [ExerciseType]
@State var itemToEdit: ExerciseType? = nil
@State var itemToDelete: ExerciseType? = nil
private func save () {
try? modelContext.save()
}
var body: some View {
NavigationStack {
Form {
List {
ForEach (items) { item in
ListItem(title: item.name)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button (role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
.navigationTitle("Exercise Types")
.sheet(item: $itemToEdit) {item in
ExerciseTypeAddEditView(model: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let itemToDelete = itemToDelete {
modelContext.delete(itemToDelete)
try? modelContext.save()
self.itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
}
}
}
}

View File

@ -1,96 +0,0 @@
import SwiftUI
import SwiftData
struct ExerciseAddEditView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var exerciseTypes: [ExerciseType]
@State var model: Exercise
init(model: Exercise? = nil) {
_model = State(initialValue: model ?? Exercise(name: "", setup: "", descr: "", sets: 3, reps: 10, weight: 30))
}
var body: some View {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section (header: Text("Exercise Type")) {
Picker("Type", selection: $model.type) {
Text("Select a type").tag(nil as ExerciseType?)
ForEach(exerciseTypes) { type in
Text(type.name).tag(type as ExerciseType?)
}
}
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
.padding(.vertical, 4)
}
Section (header: Text("Setup")) {
TextEditor(text: $model.setup)
.frame(minHeight: 100)
.padding(.vertical, 4)
// } footer: {
// Text("Describe concisely how equipment should be configured")
}
Section (header: Text("Weight")) {
HStack {
Text("\(model.weight)")
.bold()
Text("lbs")
Spacer()
Stepper("", value: $model.weight, in: 0...1000)
}
}
// Section(header: Text("Target Muscles")) {
// Button(action: {
// showingMuscleSelection = true
// }) {
// HStack {
// if selectedMuscles.isEmpty {
// Text("None selected")
// .foregroundColor(.secondary)
// } else {
// Text(selectedMuscles.map { $0.name }.joined(separator: ", "))
// .foregroundColor(.primary)
// .multilineTextAlignment(.leading)
// }
// Spacer()
// Image(systemName: "chevron.right")
// .foregroundColor(.secondary)
// .font(.caption)
// }
// }
// }
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
try? modelContext.save()
dismiss()
}
}
}
}
}

View File

@ -1,95 +0,0 @@
//
// ExercisesListView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 4:30PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct ExercisesListView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\ExerciseType.name)]) var groups: [ExerciseType]
@State var showingAddSheet = false
@State var itemToEdit: Exercise? = nil
@State var itemToDelete: Exercise? = nil
private func save () {
try? modelContext.save()
}
var body: some View {
NavigationStack {
Form {
ForEach (groups) { group in
let items = group.exercises ?? []
let itemCount = items.count
if itemCount > 0 {
Section (header: Text("\(group.name) (\(itemCount))")) {
ForEach (items) { item in
ListItem(title: item.name)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button (role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
}
}
.navigationTitle("Exercises")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showingAddSheet.toggle()
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddSheet) {
ExerciseAddEditView()
}
.sheet(item: $itemToEdit) { item in
ExerciseAddEditView(model: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let itemToDelete = itemToDelete {
modelContext.delete(itemToDelete)
try? modelContext.save()
self.itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
}
}
}
}

View File

@ -1,55 +0,0 @@
import SwiftUI
//
// MuscleGroupAddEditView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 12:14 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
struct MuscleGroupAddEditView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@State var model: MuscleGroup
init(model: MuscleGroup? = nil) {
_model = State(initialValue: model ?? MuscleGroup(name: "", descr: ""))
}
var body: some View {
NavigationStack {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
.padding(.vertical, 4)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
if model.modelContext == nil {
modelContext.insert(model)
}
try? modelContext.save()
dismiss()
}
}
}
}
}
}

View File

@ -1,81 +0,0 @@
//
// MuscleGroupsListView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 12:14 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct MuscleGroupsListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var items: [MuscleGroup]
@State var showingAddSheet = false
@State var itemToEdit: MuscleGroup? = nil
@State var itemToDelete: MuscleGroup? = nil
var body: some View {
Form {
List {
ForEach (items) { item in
ListItem(title: item.name, count: item.muscles?.count)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button (role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
.navigationTitle("Muscle Groups")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showingAddSheet.toggle()
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddSheet) {
MuscleGroupAddEditView()
}
.sheet(item: $itemToEdit) {item in
MuscleGroupAddEditView(model: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let itemToDelete = itemToDelete {
modelContext.delete(itemToDelete)
try? modelContext.save()
self.itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
}
}
}

View File

@ -1,59 +0,0 @@
//
// MuscleAddEditView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 11:55AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct MuscleAddEditView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var muscleGroups: [MuscleGroup]
@Bindable var model: Muscle
var body: some View {
NavigationStack {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Muscle Group")) {
Picker("Muscle Group", selection: $model.muscleGroup) {
Text("Select a muscle group").tag(nil as MuscleGroup?)
ForEach(muscleGroups) { group in
Text(group.name).tag(group as MuscleGroup?)
}
}
}
Section(header: Text("Description")) {
TextEditor(text: $model.descr)
.frame(minHeight: 100)
.padding(.vertical, 4)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
try? modelContext.save()
dismiss()
}
}
}
}
}
}

View File

@ -1,79 +0,0 @@
//
// MuscleGroupsListView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 12:14 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct MusclesListView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\MuscleGroup.name)]) var groups: [MuscleGroup]
@State var itemToEdit: Muscle? = nil
@State var itemToDelete: Muscle? = nil
private func save () {
try? modelContext.save()
}
var body: some View {
NavigationStack {
Form {
ForEach (groups) { group in
Section (header: Text("\(group.name) (\(group.muscles?.count ?? 0))")) {
let items = group.muscles ?? []
ForEach (items) { item in
ListItem(title: item.name)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button (role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
}
.navigationTitle("Muscles")
.sheet(item: $itemToEdit) { item in
MuscleAddEditView(model: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let itemToDelete = itemToDelete {
modelContext.delete(itemToDelete)
try? modelContext.save()
self.itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
}
}
}
}

View File

@ -75,3 +75,33 @@ struct SettingsView: View {
}
}
}
struct ExercisesListView: View {
var body: some View {
EntityListView<Exercise>(sort: [SortDescriptor(\Exercise.name)])
}
}
struct ExerciseTypeListView: View {
var body: some View {
EntityListView<ExerciseType>(sort: [SortDescriptor(\ExerciseType.name)])
}
}
struct MuscleGroupsListView: View {
var body: some View {
EntityListView<MuscleGroup>(sort: [SortDescriptor(\MuscleGroup.name)])
}
}
struct MusclesListView: View {
var body: some View {
EntityListView<Muscle>(sort: [SortDescriptor(\Muscle.name)])
}
}
struct SplitsListView: View {
var body: some View {
EntityListView<Split>(sort: [SortDescriptor(\Split.name)])
}
}

View File

@ -1,78 +0,0 @@
import SwiftUI
struct SplitAddEditView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Bindable var model: Split
@State var itemToEdit: SplitExerciseAssignment? = nil
@State var itemToDelete: SplitExerciseAssignment? = nil
var body: some View {
NavigationStack {
Form {
Section (header: Text("Name")) {
TextField("Name", text: $model.name)
.bold()
}
Section(header: Text("Description")) {
TextEditor(text: $model.intro)
.frame(minHeight: 100)
.padding(.vertical, 4)
}
Section(header: Text("Exercises")) {
let item = model
if let assignments = item.exercises, !assignments.isEmpty {
ForEach(assignments, id: \.id) { item in
List {
ListItem(
title: item.exercise?.name ?? "Unnamed",
subtitle: "\(item.sets) × \(item.reps) @ \(item.weight) lbs"
)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
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")
.foregroundColor(.secondary)
}
Button(action: {
}) {
Label("Add Exercise", systemImage: "plus.circle")
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
try? modelContext.save()
dismiss()
}
}
}
}
}
}

View File

@ -1,78 +0,0 @@
//
// SplitsListView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 10:27AM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct SplitsListView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\Split.name)]) var items: [Split]
@State var itemToEdit: Split? = nil
@State var itemToDelete: Split? = nil
private func save () {
try? modelContext.save()
}
var body: some View {
NavigationStack {
Form {
List {
ForEach (items) { item in
ListItem(
title: item.name,
count: item.exercises?.count
)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button (role: .destructive) {
itemToDelete = item
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = item
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
.navigationTitle("Muscle Groups")
}
.sheet(item: $itemToEdit) {item in
SplitAddEditView(model: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let itemToDelete = itemToDelete {
modelContext.delete(itemToDelete)
try? modelContext.save()
self.itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete \(itemToDelete?.name ?? "this item")?")
}
}
}
}

View File

@ -0,0 +1,49 @@
//
// SplitPickerView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 7:17PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct ExercisePickerView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
var onExerciseSelected: (Exercise) -> Void
var body: some View {
NavigationStack {
VStack {
Form {
Section (header: Text("This Split")) {
List {
ForEach(exercises) { exercise in
Button(action: {
onExerciseSelected(exercise)
dismiss()
}) {
ListItem(title: exercise.name)
}
.buttonStyle(.plain)
}
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
}
}
}
}

View File

@ -0,0 +1,73 @@
//
// SplitPickerView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 7:17PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct SplitPickerView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@Query(sort: [SortDescriptor(\Split.name)]) private var splits: [Split]
@Query(sort: [SortDescriptor(\Exercise.name)]) private var exercises: [Exercise]
var onSplitSelected: (Split) -> Void
// var onExerciseSelected: (Exercise) -> Void
var body: some View {
NavigationStack {
VStack {
Form {
Section (header: Text("This Split")) {
List {
ForEach(splits) { split in
Button(action: {
onSplitSelected(split)
dismiss()
}) {
HStack {
Text(split.name)
.font(.headline)
Spacer()
Text("\(split.exercises?.count ?? 0)")
.font(.caption)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}
// Section (header: Text("Additional Exercises")) {
// List {
// ForEach(exercises) { exercise in
// Button(action: {
// onExerciseSelected(exercise)
// dismiss()
// }) {
// Text(exercise.name)
// }
// .contentShape(Rectangle())
// .buttonStyle(.plain)
// }
// }
// }
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
}
}
}
}

View File

@ -0,0 +1,103 @@
//
// WorkoutAddEditView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 9:13PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct WorkoutLogEditView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State var workoutLog: WorkoutLog
@State private var showingSaveConfirmation = false
var body: some View {
Form {
Section (header: Text("Exercise")) {
Text("\(workoutLog.exercise?.name ?? "Unnamed Exercise")")
.font(.headline)
}
Section(header: Text("Sets/Reps")) {
Stepper("Sets: \(workoutLog.sets)", value: $workoutLog.sets, in: 1...10)
Stepper("Reps: \(workoutLog.reps)", value: $workoutLog.reps, in: 1...50)
}
Section(header: Text("Weight")) {
HStack {
VStack(alignment: .center) {
Text("\(workoutLog.weight) lbs")
.font(.headline)
}
Spacer()
VStack(alignment: .trailing) {
Stepper("±1", value: $workoutLog.weight, in: 0...1000)
Stepper("±5", value: $workoutLog.weight, in: 0...1000, step: 5)
}
.frame(width: 130)
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
showingSaveConfirmation = true
}
}
}
.confirmationDialog("Save Options", isPresented: $showingSaveConfirmation) {
Button("Save Workout Log Only") {
try? modelContext.save()
dismiss()
}
Button("Save Workout Log and Update Split") {
// Save the workout log
try? modelContext.save()
// Update the split with this workout log's data
// Note: Implementation depends on how splits are updated in your app
updateSplit(from: workoutLog)
dismiss()
}
Button("Cancel", role: .cancel) {
// Do nothing, dialog will dismiss
}
}
}
private func updateSplit(from workoutLog: WorkoutLog) {
let split = workoutLog.workout?.split
// Find the matching exercise in split.exercises by name
// if let exercises = split?.exercises {
// for exerciseAssignment in exercises {
// if exerciseAssignment.exercise.name == workoutLog.exercise.name {
// // Update the sets, reps, and weight in the split exercise assignment
// exerciseAssignment.sets = workoutLog.sets
// exerciseAssignment.reps = workoutLog.reps
// exerciseAssignment.weight = workoutLog.weight
//
// // Save the changes to the split
// try? modelContext.save()
// break
// }
// }
// }
}
}

View File

@ -0,0 +1,125 @@
//
// WorkoutLogView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 6:58PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
struct WorkoutLogView: View {
@Environment(\.modelContext) private var modelContext
@State var workout: Workout
@State private var showingAddSheet = false
@State private var itemToEdit: WorkoutLog? = nil
@State private var itemToDelete: WorkoutLog? = nil
var sortedWorkoutLogs: [WorkoutLog] {
if let logs = workout.logs {
logs.sorted(by: { $0.exercise!.name < $1.exercise!.name })
} else {
[]
}
}
var body: some View {
Form {
List {
ForEach (sortedWorkoutLogs) { log in
let badges = log.completed ? [Badge(text: "Completed", color: .green)] : []
ListItem(
title: log.exercise?.name ?? "Untitled Exercise",
subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs",
badges: badges
)
.swipeActions(edge: .leading, allowsFullSwipe: false) {
if (log.completed) {
Button {
log.completed = false
try? modelContext.save()
} label: {
Label("Complete", systemImage: "circle.fill")
}
.tint(.green)
} else {
Button {
log.completed = true
try? modelContext.save()
} label: {
Label("Reset", systemImage: "checkmark.circle.fill")
}
.tint(.green)
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
itemToDelete = log
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = log
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
.navigationTitle("Workout")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showingAddSheet.toggle() }) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddSheet) {
ExercisePickerView { exercise in
let workoutLog = WorkoutLog(
workout: workout,
exercise: exercise,
date: Date(),
sets: exercise.sets,
reps: exercise.reps,
weight: exercise.weight,
completed: false
)
workout.logs?.append(workoutLog)
try? modelContext.save()
}
}
.sheet(item: $itemToEdit) { item in
WorkoutLogEditView(workoutLog: item)
}
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
withAnimation {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete workout started \(itemToDelete?.exercise?.name ?? "this item")?")
}
}
}

View File

@ -0,0 +1,11 @@
import SwiftUI
struct WorkoutView: View {
var body: some View {
Text("Workout View")
}
}
#Preview {
WorkoutView()
}

View File

@ -0,0 +1,150 @@
//
// WorkoutsView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 6:52PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
import SwiftUI
import SwiftData
struct WorkoutsView: View {
private let logger = AppLogger(
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
category: "WorkoutsView"
)
@Environment(\.modelContext) private var modelContext
@Query(sort: [SortDescriptor(\Workout.start, order: .reverse)]) var workouts: [Workout]
@State private var showingSplitPicker = false
@State private var itemToDelete: Workout? = nil
@State private var itemToEdit: Workout? = nil
var body: some View {
NavigationStack {
Form {
if workouts.isEmpty {
Text("No workouts yet")
} else {
List {
ForEach (workouts) { workout in
NavigationLink(destination: WorkoutLogView(workout: workout)) {
ListItem(title: workout.label)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
itemToDelete = workout
} label: {
Label("Delete", systemImage: "trash")
}
Button {
itemToEdit = workout
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.indigo)
}
}
}
}
}
.navigationTitle("Workouts")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Start Workout") {
showingSplitPicker = true
}
}
}
// .sheet(item: $itemToEdit) { item in
// T.formView(for: item)
// }
.confirmationDialog(
"Delete?",
isPresented: Binding<Bool>(
get: { itemToDelete != nil },
set: { if !$0 { itemToDelete = nil } }
),
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
if let item = itemToDelete {
modelContext.delete(item)
try? modelContext.save()
itemToDelete = nil
}
}
Button("Cancel", role: .cancel) {
itemToDelete = nil
}
} message: {
Text("Are you sure you want to delete this workout?")
}
.sheet(isPresented: $showingSplitPicker) {
SplitPickerView { split in
let workout = Workout(start: Date(), split: split)
modelContext.insert(workout)
if let exercises = split.exercises {
for assignment in exercises {
if let exercise = assignment.exercise {
let workoutLog = WorkoutLog(
workout: workout,
exercise: exercise,
date: Date(),
sets: assignment.sets,
reps: assignment.reps,
weight: assignment.weight,
completed: false
)
modelContext.insert(workoutLog)
} else {
logger.debug("An exercise entity for a split is nil")
}
}
}
try? modelContext.save()
}
}
}
}
}
extension Date {
func formattedDate() -> String {
let calendar = Calendar.current
let now = Date()
let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "h:mm a"
let dateFormatter = DateFormatter()
let date = self
if calendar.isDateInToday(date) {
return "Today @ \(timeFormatter.string(from: date))"
} else if calendar.isDateInYesterday(date) {
return "Yesterday @ \(timeFormatter.string(from: date))"
} else {
let dateComponents = calendar.dateComponents([.year], from: date)
let currentYearComponents = calendar.dateComponents([.year], from: now)
if dateComponents.year == currentYearComponents.year {
dateFormatter.dateFormat = "M/d"
} else {
dateFormatter.dateFormat = "M/d/yyyy"
}
let dateString = dateFormatter.string(from: date)
let timeString = timeFormatter.string(from: date)
return "\(dateString) @ \(timeString)"
}
}
}