wip
This commit is contained in:
@ -13,6 +13,7 @@ struct CalendarListItem: View {
|
||||
var date: Date
|
||||
var title: String
|
||||
var subtitle: String?
|
||||
var subtitle2: String?
|
||||
var count: Int?
|
||||
|
||||
var body: some View {
|
||||
@ -31,7 +32,7 @@ struct CalendarListItem: View {
|
||||
}
|
||||
.padding([.trailing], 10)
|
||||
}
|
||||
HStack {
|
||||
HStack (alignment: .top) {
|
||||
VStack (alignment: .leading) {
|
||||
Text("\(title)")
|
||||
.font(.headline)
|
||||
@ -39,6 +40,10 @@ struct CalendarListItem: View {
|
||||
Text("\(subtitle)")
|
||||
.font(.footnote)
|
||||
}
|
||||
if let subtitle = subtitle2 {
|
||||
Text("\(subtitle)")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
if let count = count {
|
||||
Spacer()
|
71
Workouts/Views/Common/CheckboxListItem.swift
Normal file
71
Workouts/Views/Common/CheckboxListItem.swift
Normal file
@ -0,0 +1,71 @@
|
||||
//
|
||||
// ListItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:42 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum CheckboxStatus {
|
||||
case checked
|
||||
case unchecked
|
||||
case intermediate
|
||||
case cancelled
|
||||
|
||||
var color: Color {
|
||||
switch (self) {
|
||||
case .checked: .green
|
||||
case .unchecked: .gray
|
||||
case .intermediate: .yellow
|
||||
case .cancelled: .red
|
||||
}
|
||||
}
|
||||
|
||||
var systemName: String {
|
||||
switch (self) {
|
||||
case .checked: "checkmark.circle.fill"
|
||||
case .unchecked: "circle"
|
||||
case .intermediate: "ellipsis.circle"
|
||||
case .cancelled: "cross.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckboxListItem: View {
|
||||
var status: CheckboxStatus
|
||||
var title: String
|
||||
var subtitle: String?
|
||||
var count: Int?
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
Image(systemName: status.systemName)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 30)
|
||||
.foregroundStyle(status.color)
|
||||
VStack (alignment: .leading) {
|
||||
Text("\(title)")
|
||||
.font(.headline)
|
||||
HStack (alignment: .bottom) {
|
||||
if let subtitle = subtitle {
|
||||
Text("\(subtitle)")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let count = count {
|
||||
Spacer()
|
||||
Text("\(count)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
57
Workouts/Views/Common/ListItem.swift
Normal file
57
Workouts/Views/Common/ListItem.swift
Normal file
@ -0,0 +1,57 @@
|
||||
//
|
||||
// ListItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:42 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListItem: View {
|
||||
var title: String?
|
||||
var text: String?
|
||||
var subtitle: String?
|
||||
var count: Int?
|
||||
// var badges: [Badge]? = []
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack (alignment: .leading) {
|
||||
if let title = title {
|
||||
Text("\(title)")
|
||||
.font(.headline)
|
||||
if let text = text {
|
||||
Text("\(text)")
|
||||
.font(.footnote)
|
||||
}
|
||||
} else {
|
||||
if let text = text {
|
||||
Text("\(text)")
|
||||
}
|
||||
}
|
||||
HStack (alignment: .bottom) {
|
||||
// if let badges = badges {
|
||||
// ForEach (badges, id: \.self) { badge in
|
||||
// BadgeView(badge: badge)
|
||||
// }
|
||||
// }
|
||||
if let subtitle = subtitle {
|
||||
Text("\(subtitle)")
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let count = count {
|
||||
Spacer()
|
||||
Text("\(count)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,12 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SplitExerciseAssignmentAddEditView: View {
|
||||
struct ExerciseAddEditView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var showingExercisePicker = false
|
||||
|
||||
@State var model: SplitExerciseAssignment
|
||||
@State var model: Exercise
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@ -24,7 +24,7 @@ struct SplitExerciseAssignmentAddEditView: View {
|
||||
showingExercisePicker = true
|
||||
}) {
|
||||
HStack {
|
||||
Text(model.exerciseName.isEmpty ? "Select Exercise" : model.exerciseName)
|
||||
Text(model.name.isEmpty ? "Select Exercise" : model.name)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.gray)
|
||||
@ -54,11 +54,11 @@ struct SplitExerciseAssignmentAddEditView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingExercisePicker) {
|
||||
ExercisePickerView { exerciseName in
|
||||
model.exerciseName = exerciseName
|
||||
ExercisePickerView { exerciseNames in
|
||||
model.name = exerciseNames.first ?? "Exercise.unnamed"
|
||||
}
|
||||
}
|
||||
.navigationTitle(model.exerciseName.isEmpty ? "New Exercise" : model.exerciseName)
|
||||
.navigationTitle(model.name.isEmpty ? "New Exercise" : model.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
68
Workouts/Views/Exercises/ExerciseListLoader.swift
Normal file
68
Workouts/Views/Exercises/ExerciseListLoader.swift
Normal file
@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
import Yams
|
||||
|
||||
class ExerciseListLoader {
|
||||
struct ExerciseListData: Codable {
|
||||
let name: String
|
||||
let source: String
|
||||
let exercises: [ExerciseItem]
|
||||
|
||||
struct ExerciseItem: Codable, Identifiable {
|
||||
let name: String
|
||||
let descr: String
|
||||
let type: String
|
||||
let split: String
|
||||
|
||||
var id: String { name }
|
||||
}
|
||||
}
|
||||
|
||||
static func loadExerciseLists() -> [String: ExerciseListData] {
|
||||
var exerciseLists: [String: ExerciseListData] = [:]
|
||||
|
||||
guard let resourcePath = Bundle.main.resourcePath else {
|
||||
print("Could not find resource path")
|
||||
return exerciseLists
|
||||
}
|
||||
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let resourceURL = URL(fileURLWithPath: resourcePath)
|
||||
let yamlFiles = try fileManager.contentsOfDirectory(at: resourceURL, includingPropertiesForKeys: nil)
|
||||
.filter { $0.pathExtension == "yaml" && $0.lastPathComponent.hasSuffix(".exercises.yaml") }
|
||||
|
||||
for yamlFile in yamlFiles {
|
||||
let fileName = yamlFile.lastPathComponent
|
||||
do {
|
||||
let yamlString = try String(contentsOf: yamlFile, encoding: .utf8)
|
||||
if let exerciseList = try Yams.load(yaml: yamlString) as? [String: Any],
|
||||
let name = exerciseList["name"] as? String,
|
||||
let source = exerciseList["source"] as? String,
|
||||
let exercisesData = exerciseList["exercises"] as? [[String: Any]] {
|
||||
|
||||
var exercises: [ExerciseListData.ExerciseItem] = []
|
||||
|
||||
for exerciseData in exercisesData {
|
||||
if let name = exerciseData["name"] as? String,
|
||||
let descr = exerciseData["descr"] as? String,
|
||||
let type = exerciseData["type"] as? String,
|
||||
let split = exerciseData["split"] as? String {
|
||||
let exercise = ExerciseListData.ExerciseItem(name: name, descr: descr, type: type, split: split)
|
||||
exercises.append(exercise)
|
||||
}
|
||||
}
|
||||
|
||||
let exerciseList = ExerciseListData(name: name, source: source, exercises: exercises)
|
||||
exerciseLists[fileName] = exerciseList
|
||||
}
|
||||
} catch {
|
||||
print("Error loading YAML file \(fileName): \(error)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Error listing directory contents: \(error)")
|
||||
}
|
||||
|
||||
return exerciseLists
|
||||
}
|
||||
}
|
153
Workouts/Views/Exercises/ExercisePickerView.swift
Normal file
153
Workouts/Views/Exercises/ExercisePickerView.swift
Normal file
@ -0,0 +1,153 @@
|
||||
//
|
||||
// ExercisePickerView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 7:17 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExercisePickerView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var exerciseLists: [String: ExerciseListLoader.ExerciseListData] = [:]
|
||||
@State private var selectedListName: String? = nil
|
||||
@State private var selectedExercises: Set<String> = []
|
||||
|
||||
var onExerciseSelected: ([String]) -> Void
|
||||
var allowMultiSelect: Bool = false
|
||||
|
||||
init(onExerciseSelected: @escaping ([String]) -> Void, allowMultiSelect: Bool = false) {
|
||||
self.onExerciseSelected = onExerciseSelected
|
||||
self.allowMultiSelect = allowMultiSelect
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
Text("Multi-Select: \(allowMultiSelect)")
|
||||
if selectedListName == nil {
|
||||
// Show list of exercise list files
|
||||
List {
|
||||
ForEach(Array(exerciseLists.keys.sorted()), id: \.self) { fileName in
|
||||
if let list = exerciseLists[fileName] {
|
||||
Button(action: {
|
||||
selectedListName = fileName
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(list.name)
|
||||
.font(.headline)
|
||||
Text(list.source)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(list.exercises.count) exercises")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Exercise Lists")
|
||||
} else if let fileName = selectedListName, let list = exerciseLists[fileName] {
|
||||
// Show exercises in the selected list grouped by split
|
||||
List {
|
||||
// Group exercises by split
|
||||
let exercisesByGroup = Dictionary(grouping: list.exercises) { $0.split }
|
||||
let sortedGroups = exercisesByGroup.keys.sorted()
|
||||
|
||||
ForEach(sortedGroups, id: \.self) { splitName in
|
||||
Section(header: Text(splitName)) {
|
||||
ForEach(exercisesByGroup[splitName]?.sorted(by: { $0.name < $1.name }) ?? [], id: \.id) { exercise in
|
||||
if allowMultiSelect {
|
||||
Button(action: {
|
||||
if selectedExercises.contains(exercise.name) {
|
||||
selectedExercises.remove(exercise.name)
|
||||
} else {
|
||||
selectedExercises.insert(exercise.name)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(exercise.name)
|
||||
.font(.headline)
|
||||
Text(exercise.type)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
||||
Spacer()
|
||||
|
||||
if selectedExercises.contains(exercise.name) {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
onExerciseSelected([exercise.name])
|
||||
dismiss()
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(exercise.name)
|
||||
.font(.headline)
|
||||
Text(exercise.type)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(list.name)
|
||||
.toolbar {
|
||||
if let _ = selectedListName {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Back") {
|
||||
selectedListName = nil
|
||||
selectedExercises.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowMultiSelect {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Select") {
|
||||
if !selectedExercises.isEmpty {
|
||||
onExerciseSelected(Array(selectedExercises))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.disabled(selectedExercises.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
if let _ = selectedListName {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadExerciseLists()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadExerciseLists() {
|
||||
exerciseLists = ExerciseListLoader.loadExerciseLists()
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 10:24 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import CloudKit
|
||||
|
||||
enum AppStorageKeys {
|
||||
static let iCloudSyncEnabled = "iCloudSyncEnabled"
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@State private var showingClearAllDataConfirmation = false
|
||||
|
||||
var splitsCount: Int? { try? modelContext.fetchCount(FetchDescriptor<Split>()) }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section (header: Text("Lists")) {
|
||||
NavigationLink(destination: SplitsListView()) {
|
||||
HStack {
|
||||
Text("Splits")
|
||||
Spacer()
|
||||
Text("\(splitsCount ?? 0)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Developer")) {
|
||||
Button(action: {
|
||||
showingClearAllDataConfirmation = true
|
||||
}) {
|
||||
HStack {
|
||||
Label("Clear All Data", systemImage: "trash")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.confirmationDialog(
|
||||
"Clear All Data?",
|
||||
isPresented: $showingClearAllDataConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Clear All Data", role: .destructive) {
|
||||
clearAllData()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text("This action cannot be undone. All data will be permanently deleted.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAllObjects<T: PersistentModel>(ofType type: T.Type, from context: ModelContext) throws {
|
||||
let descriptor = FetchDescriptor<T>()
|
||||
let allObjects = try context.fetch(descriptor)
|
||||
for object in allObjects {
|
||||
context.delete(object)
|
||||
}
|
||||
try context.save()
|
||||
}
|
||||
|
||||
private func clearAllData () {
|
||||
do {
|
||||
try deleteAllObjects(ofType: Split.self, from: modelContext)
|
||||
try deleteAllObjects(ofType: SplitExerciseAssignment.self, from: modelContext)
|
||||
try deleteAllObjects(ofType: Workout.self, from: modelContext)
|
||||
try deleteAllObjects(ofType: WorkoutLog.self, from: modelContext)
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Failed to clear all data: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SplitsListView: View {
|
||||
var body: some View {
|
||||
EntityListView<Split>(sort: [SortDescriptor(\Split.name)])
|
||||
}
|
||||
}
|
@ -10,35 +10,51 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct SplitExercisesListView: View {
|
||||
struct ExerciseListView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var model: Split
|
||||
// Use a @Query to observe the Split and its exercises
|
||||
@Query private var splits: [Split]
|
||||
private var split: Split
|
||||
|
||||
@State private var showingAddSheet: Bool = false
|
||||
@State private var itemToEdit: SplitExerciseAssignment? = nil
|
||||
@State private var itemToDelete: SplitExerciseAssignment? = nil
|
||||
@State private var itemToEdit: Exercise? = nil
|
||||
@State private var itemToDelete: Exercise? = nil
|
||||
@State private var createdWorkout: Workout? = nil
|
||||
|
||||
// Initialize with a Split and set up a query to observe it
|
||||
init(split: Split) {
|
||||
self.split = split
|
||||
// Create a predicate to fetch only this specific split
|
||||
let splitId = split.persistentModelID
|
||||
self._splits = Query(filter: #Predicate<Split> { s in
|
||||
s.persistentModelID == splitId
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Use the first Split from our query if available, otherwise fall back to the original split
|
||||
let currentSplit = splits.first ?? split
|
||||
|
||||
NavigationStack {
|
||||
Form {
|
||||
List {
|
||||
if let assignments = model.exercises, !assignments.isEmpty {
|
||||
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.exerciseName < $1.exerciseName : $0.order < $1.order })
|
||||
if let assignments = currentSplit.exercises, !assignments.isEmpty {
|
||||
let sortedAssignments = assignments.sorted(by: { $0.order == $1.order ? $0.name < $1.name : $0.order < $1.order })
|
||||
|
||||
ForEach(sortedAssignments) { item in
|
||||
ListItem(
|
||||
title: item.exerciseName,
|
||||
title: item.name,
|
||||
subtitle: "\(item.sets) × \(item.reps) × \(item.weight) lbs \(item.order)"
|
||||
)
|
||||
.swipeActions {
|
||||
Button {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
Label("Delete", systemImage: "circle")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
@ -76,19 +92,19 @@ struct SplitExercisesListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("\(model.name)")
|
||||
.navigationTitle("\(currentSplit.name)")
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button("Start This Split") {
|
||||
let split = model
|
||||
let workout = Workout(start: Date(), split: split)
|
||||
let split = currentSplit
|
||||
let workout = Workout(start: Date(), end: Date(), split: split)
|
||||
modelContext.insert(workout)
|
||||
if let exercises = split.exercises {
|
||||
for assignment in exercises {
|
||||
let workoutLog = WorkoutLog(
|
||||
workout: workout,
|
||||
exerciseName: assignment.exerciseName,
|
||||
exerciseName: assignment.name,
|
||||
date: Date(),
|
||||
order: assignment.order,
|
||||
sets: assignment.sets,
|
||||
@ -106,7 +122,7 @@ struct SplitExercisesListView: View {
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $createdWorkout, destination: { workout in
|
||||
WorkoutLogView(workout: workout)
|
||||
WorkoutLogListView(workout: workout)
|
||||
})
|
||||
// .toolbar {
|
||||
// ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@ -116,19 +132,49 @@ struct SplitExercisesListView: View {
|
||||
// }
|
||||
// }
|
||||
.sheet (isPresented: $showingAddSheet) {
|
||||
ExercisePickerView { exerciseName in
|
||||
itemToEdit = SplitExerciseAssignment(
|
||||
split: model,
|
||||
exerciseName: exerciseName,
|
||||
order: 0,
|
||||
sets: 3,
|
||||
reps: 10,
|
||||
weight: 40
|
||||
)
|
||||
}
|
||||
ExercisePickerView(onExerciseSelected: { exerciseNames in
|
||||
let splitId = currentSplit.persistentModelID
|
||||
print("exerciseNames: \(exerciseNames)")
|
||||
if exerciseNames.count == 1 {
|
||||
itemToEdit = Exercise(
|
||||
split: split,
|
||||
exerciseName: exerciseNames.first ?? "Exercise.unnamed",
|
||||
order: 0,
|
||||
sets: 3,
|
||||
reps: 10,
|
||||
weight: 40
|
||||
)
|
||||
} else {
|
||||
for exerciseName in exerciseNames {
|
||||
var duplicateExercise: [Exercise]? = nil
|
||||
do {
|
||||
duplicateExercise = try modelContext.fetch(FetchDescriptor<Exercise>(predicate: #Predicate{ exercise in
|
||||
exerciseName == exercise.name && splitId == exercise.split?.persistentModelID
|
||||
}))
|
||||
} catch {
|
||||
print("ERROR: failed to fetch \(exerciseName)")
|
||||
}
|
||||
|
||||
if let dup = duplicateExercise, dup.count > 0 {
|
||||
print("Skipping duplicate \(exerciseName) found \(dup.count) duplicate(s)")
|
||||
} else {
|
||||
print("Creating \(exerciseName) for \(split.name)")
|
||||
modelContext.insert(Exercise(
|
||||
split: split,
|
||||
exerciseName: exerciseName,
|
||||
order: 0,
|
||||
sets: 3,
|
||||
reps: 10,
|
||||
weight: 40
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
try? modelContext.save()
|
||||
}, allowMultiSelect: true)
|
||||
}
|
||||
.sheet(item: $itemToEdit) { item in
|
||||
SplitExerciseAssignmentAddEditView(model: item)
|
||||
ExerciseAddEditView(model: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete Exercise?",
|
30
Workouts/Views/Splits/OrderableItem.swift
Normal file
30
Workouts/Views/Splits/OrderableItem.swift
Normal file
@ -0,0 +1,30 @@
|
||||
//
|
||||
// OrderableItem.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/18/25 at 5:19 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Protocol for items that can be ordered in a sequence
|
||||
protocol OrderableItem {
|
||||
/// Updates the order of the item to the specified index
|
||||
func updateOrder(to index: Int)
|
||||
}
|
||||
|
||||
/// Extension to make Split conform to OrderableItem
|
||||
extension Split: OrderableItem {
|
||||
func updateOrder(to index: Int) {
|
||||
self.order = index
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to make SplitExerciseAssignment conform to OrderableItem
|
||||
extension Exercise: OrderableItem {
|
||||
func updateOrder(to index: Int) {
|
||||
self.order = index
|
||||
}
|
||||
}
|
@ -10,6 +10,9 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SplitAddEditView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var model: Split
|
||||
|
||||
private let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
|
||||
@ -17,46 +20,62 @@ struct SplitAddEditView: View {
|
||||
private let availableIcons = ["dumbbell.fill", "figure.strengthtraining.traditional", "figure.run", "figure.hiking", "figure.cooldown", "figure.boxing", "figure.wrestling", "figure.gymnastics", "figure.handball", "figure.core.training", "heart.fill", "bolt.fill"]
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Name")) {
|
||||
TextField("Name", text: $model.name)
|
||||
.bold()
|
||||
}
|
||||
|
||||
Section(header: Text("Appearance")) {
|
||||
Picker("Color", selection: $model.color) {
|
||||
ForEach(availableColors, id: \.self) { colorName in
|
||||
let tempSplit = Split(name: "", color: colorName)
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(tempSplit.getColor())
|
||||
.frame(width: 20, height: 20)
|
||||
Text(colorName.capitalized)
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Name")) {
|
||||
TextField("Name", text: $model.name)
|
||||
.bold()
|
||||
}
|
||||
|
||||
Section(header: Text("Appearance")) {
|
||||
Picker("Color", selection: $model.color) {
|
||||
ForEach(availableColors, id: \.self) { colorName in
|
||||
let tempSplit = Split(name: "", color: colorName)
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(tempSplit.getColor())
|
||||
.frame(width: 20, height: 20)
|
||||
Text(colorName.capitalized)
|
||||
}
|
||||
.tag(colorName)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Icon", selection: $model.systemImage) {
|
||||
ForEach(availableIcons, id: \.self) { iconName in
|
||||
HStack {
|
||||
Image(systemName: iconName)
|
||||
.frame(width: 24, height: 24)
|
||||
Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized)
|
||||
}
|
||||
.tag(iconName)
|
||||
}
|
||||
.tag(colorName)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Icon", selection: $model.systemImage) {
|
||||
ForEach(availableIcons, id: \.self) { iconName in
|
||||
HStack {
|
||||
Image(systemName: iconName)
|
||||
.frame(width: 24, height: 24)
|
||||
Text(iconName.replacingOccurrences(of: ".fill", with: "").replacingOccurrences(of: "figure.", with: "").capitalized)
|
||||
}
|
||||
.tag(iconName)
|
||||
Section(header: Text("Exercises")) {
|
||||
NavigationLink {
|
||||
ExerciseListView(split: model)
|
||||
} label: {
|
||||
ListItem(
|
||||
text: "Exercises",
|
||||
count: model.exercises?.count ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Exercises")) {
|
||||
NavigationLink {
|
||||
SplitExercisesListView(model: model)
|
||||
} label: {
|
||||
ListItem(
|
||||
text: "Exercises",
|
||||
count: model.exercises?.count ?? 0
|
||||
)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DraggableSplitItem: View {
|
||||
struct SplitItem: View {
|
||||
|
||||
var name: String
|
||||
var color: Color
|
||||
@ -31,15 +31,15 @@ struct DraggableSplitItem: View {
|
||||
.aspectRatio(1.618, contentMode: .fit)
|
||||
.shadow(radius: 2)
|
||||
|
||||
GeometryReader { geometry in
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 4) {
|
||||
Spacer()
|
||||
|
||||
// Icon in the center - now using dynamic sizing
|
||||
Image(systemName: systemImageName)
|
||||
.font(.system(size: min(geometry.size.width * 0.3, 40), weight: .bold))
|
||||
.font(.system(size: min(geo.size.width * 0.3, 40), weight: .bold))
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: geometry.size.width * 0.6, maxHeight: geometry.size.height * 0.4)
|
||||
.frame(maxWidth: geo.size.width * 0.6, maxHeight: geo.size.height * 0.4)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Name at the bottom inside the rectangle
|
||||
@ -53,7 +53,7 @@ struct DraggableSplitItem: View {
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
}
|
||||
}
|
||||
}
|
@ -25,9 +25,9 @@ struct SplitsView: View {
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
|
||||
SortableForEach($splits, allowReordering: $allowSorting) { split, dragging in
|
||||
NavigationLink {
|
||||
SplitExercisesListView(model: split)
|
||||
ExerciseListView(split: split)
|
||||
} label: {
|
||||
DraggableSplitItem(
|
||||
SplitItem(
|
||||
name: split.name,
|
||||
color: Color.color(from: split.color),
|
||||
systemImageName: split.systemImage,
|
||||
@ -40,18 +40,7 @@ struct SplitsView: View {
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Splits")
|
||||
.onAppear {
|
||||
do {
|
||||
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
|
||||
sortBy: [
|
||||
SortDescriptor(\Split.order),
|
||||
SortDescriptor(\Split.name)
|
||||
]
|
||||
))
|
||||
} catch {
|
||||
print("ERROR: failed to load splits \(error)")
|
||||
}
|
||||
}
|
||||
.onAppear(perform: loadSplits)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: { showingAddSheet.toggle() }) {
|
||||
@ -65,4 +54,17 @@ struct SplitsView: View {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func loadSplits () {
|
||||
do {
|
||||
self.splits = try modelContext.fetch(FetchDescriptor<Split>(
|
||||
sortBy: [
|
||||
SortDescriptor(\Split.order),
|
||||
SortDescriptor(\Split.name)
|
||||
]
|
||||
))
|
||||
} catch {
|
||||
print("ERROR: failed to load splits \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,12 +87,12 @@ struct WorkoutLogEditView: View {
|
||||
|
||||
// Find the matching exercise in split.exercises by name
|
||||
if let exercises = split?.exercises {
|
||||
for exerciseAssignment in exercises {
|
||||
if exerciseAssignment.exerciseName == workoutLog.exerciseName {
|
||||
for exercise in exercises {
|
||||
if exercise.name == workoutLog.exerciseName {
|
||||
// Update the sets, reps, and weight in the split exercise assignment
|
||||
exerciseAssignment.sets = workoutLog.sets
|
||||
exerciseAssignment.reps = workoutLog.reps
|
||||
exerciseAssignment.weight = workoutLog.weight
|
||||
exercise.sets = workoutLog.sets
|
||||
exercise.reps = workoutLog.reps
|
||||
exercise.weight = workoutLog.weight
|
||||
|
||||
// Save the changes to the split
|
||||
try? modelContext.save()
|
@ -10,7 +10,7 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutLogView: View {
|
||||
struct WorkoutLogListView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
@State var workout: Workout
|
||||
@ -47,10 +47,7 @@ struct WorkoutLogView: View {
|
||||
|
||||
if [.inProgress,.completed].contains(status) {
|
||||
Button {
|
||||
withAnimation {
|
||||
log.status = .notStarted
|
||||
try? modelContext.save()
|
||||
}
|
||||
resetWorkout(log)
|
||||
} label: {
|
||||
Label("Not Started", systemImage: WorkoutStatus.notStarted.checkboxStatus.systemName)
|
||||
}
|
||||
@ -59,10 +56,7 @@ struct WorkoutLogView: View {
|
||||
|
||||
if [.notStarted,.completed].contains(status) {
|
||||
Button {
|
||||
withAnimation {
|
||||
log.status = .inProgress
|
||||
try? modelContext.save()
|
||||
}
|
||||
startWorkout(log)
|
||||
} label: {
|
||||
Label("In Progress", systemImage: WorkoutStatus.inProgress.checkboxStatus.systemName)
|
||||
}
|
||||
@ -71,10 +65,7 @@ struct WorkoutLogView: View {
|
||||
|
||||
if [.notStarted,.inProgress].contains(status) {
|
||||
Button {
|
||||
withAnimation {
|
||||
log.status = .completed
|
||||
try? modelContext.save()
|
||||
}
|
||||
completeWorkout(log)
|
||||
} label: {
|
||||
Label("Complete", systemImage: WorkoutStatus.completed.checkboxStatus.systemName)
|
||||
}
|
||||
@ -110,11 +101,11 @@ struct WorkoutLogView: View {
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddSheet) {
|
||||
ExercisePickerView { exerciseName in
|
||||
let setsRepsWeight = getSetsRepsWeight(exerciseName, in: modelContext)
|
||||
ExercisePickerView { exerciseNames in
|
||||
let setsRepsWeight = getSetsRepsWeight(exerciseNames.first ?? "Exercise.unnamed", in: modelContext)
|
||||
let workoutLog = WorkoutLog(
|
||||
workout: workout,
|
||||
exerciseName: exerciseName,
|
||||
exerciseName: exerciseNames.first ?? "Exercise.unnamed",
|
||||
date: Date(),
|
||||
sets: setsRepsWeight.sets,
|
||||
reps: setsRepsWeight.reps,
|
||||
@ -154,6 +145,46 @@ struct WorkoutLogView: View {
|
||||
|
||||
}
|
||||
|
||||
func startWorkout (_ log: WorkoutLog) {
|
||||
withAnimation {
|
||||
log.status = .inProgress
|
||||
updateWorkout(log)
|
||||
}
|
||||
}
|
||||
|
||||
func resetWorkout (_ log: WorkoutLog) {
|
||||
withAnimation {
|
||||
log.status = .notStarted
|
||||
updateWorkout(log)
|
||||
}
|
||||
}
|
||||
|
||||
func completeWorkout (_ log: WorkoutLog) {
|
||||
withAnimation {
|
||||
log.status = .completed
|
||||
updateWorkout(log)
|
||||
}
|
||||
}
|
||||
|
||||
func updateWorkout (_ log: WorkoutLog) {
|
||||
if let workout = log.workout {
|
||||
if let _ = workout.logs?.first(where: { $0.status != .completed }) {
|
||||
if let notStartedLogs = workout.logs?.filter({ $0.status == .notStarted }) {
|
||||
if notStartedLogs.count == workout.logs?.count ?? 0 {
|
||||
workout.status = .notStarted
|
||||
}
|
||||
}
|
||||
if let _ = workout.logs?.first(where: { $0.status == .inProgress }) {
|
||||
workout.status = .inProgress
|
||||
}
|
||||
} else {
|
||||
workout.status = .completed
|
||||
workout.end = Date()
|
||||
}
|
||||
try? modelContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
func getSetsRepsWeight(_ exerciseName: String, in modelContext: ModelContext) -> SetsRepsWeight {
|
||||
// Use a single expression predicate that works with SwiftData
|
||||
print("Searching for exercise name: \(exerciseName)")
|
35
Workouts/Views/WorkoutLog/WorkoutStatus.swift
Normal file
35
Workouts/Views/WorkoutLog/WorkoutStatus.swift
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// WorkoutStatus.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/16/25 at 7:03 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
enum WorkoutStatus: Int, Codable {
|
||||
case notStarted = 1
|
||||
case inProgress = 2
|
||||
case completed = 3
|
||||
case skipped = 4
|
||||
|
||||
static var unnamed = "Undetermined"
|
||||
|
||||
var name: String {
|
||||
switch (self) {
|
||||
case .notStarted: "Not Started"
|
||||
case .inProgress: "In Progress"
|
||||
case .completed: "Completed"
|
||||
case .skipped: "Skipped"
|
||||
}
|
||||
}
|
||||
|
||||
var checkboxStatus: CheckboxStatus {
|
||||
switch (self) {
|
||||
case .notStarted: .unchecked
|
||||
case .inProgress: .intermediate
|
||||
case .completed: .checked
|
||||
case .skipped: .cancelled
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
//
|
||||
// ExercisePickerView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/13/25 at 7:17 PM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ExercisePickerView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var exerciseLists: [String: ExerciseList] = [:]
|
||||
@State private var selectedListName: String? = nil
|
||||
|
||||
var onExerciseSelected: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if selectedListName == nil {
|
||||
// Show list of exercise list files
|
||||
List {
|
||||
ForEach(Array(exerciseLists.keys.sorted()), id: \.self) { fileName in
|
||||
if let list = exerciseLists[fileName] {
|
||||
Button(action: {
|
||||
selectedListName = fileName
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(list.name)
|
||||
.font(.headline)
|
||||
Text(list.source)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(list.exercises.count) exercises")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Exercise Lists")
|
||||
} else if let fileName = selectedListName, let list = exerciseLists[fileName] {
|
||||
// Show exercises in the selected list
|
||||
List {
|
||||
ForEach(list.exercises) { exercise in
|
||||
Button(action: {
|
||||
onExerciseSelected(exercise.name)
|
||||
dismiss()
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(exercise.name)
|
||||
.font(.headline)
|
||||
Text(exercise.type)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(list.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Back") {
|
||||
selectedListName = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadExerciseLists()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadExerciseLists() {
|
||||
exerciseLists = ExerciseListLoader.loadExerciseLists()
|
||||
}
|
||||
}
|
@ -20,44 +20,20 @@ struct WorkoutEditView: View {
|
||||
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("Status")) {
|
||||
Text("\(workout.status?.name ?? WorkoutStatus.unnamed)")
|
||||
}
|
||||
|
||||
Section (header: Text("Start/End")) {
|
||||
DatePicker("Started", selection: $workout.start)
|
||||
Toggle("Workout Ended", isOn: Binding(
|
||||
get: { workout.end != nil },
|
||||
set: { newValue in
|
||||
withAnimation {
|
||||
if newValue {
|
||||
workoutEndDate = Date()
|
||||
workout.end = workoutEndDate
|
||||
} else {
|
||||
workout.end = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
if workout.end != nil {
|
||||
if workout.status == .completed {
|
||||
DatePicker("Ended", selection: $workoutEndDate)
|
||||
}
|
||||
}
|
||||
|
||||
// Section (header: Text("Workout Log")) {
|
||||
// if let workoutLogs = workout.logs {
|
||||
// List {
|
||||
// ForEach (workoutLogs) { log in
|
||||
// ListItem(
|
||||
// title: log.exerciseName,
|
||||
// subtitle: "\(log.sets) sets × \(log.reps) reps × \(log.weight) lbs"
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// Text("No workout logs yet")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed) Split")
|
||||
.toolbar {
|
||||
@ -70,6 +46,9 @@ struct WorkoutEditView: View {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
try? modelContext.save()
|
||||
if workout.status == .completed {
|
||||
workout.end = workoutEndDate
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct WorkoutsView: View {
|
||||
struct WorkoutListView: View {
|
||||
private let logger = AppLogger(
|
||||
subsystem: Bundle.main.bundleIdentifier ?? "dev.rzen.indie.Workouts",
|
||||
category: "WorkoutsView"
|
||||
@ -33,11 +33,12 @@ struct WorkoutsView: View {
|
||||
} else {
|
||||
List {
|
||||
ForEach (workouts) { workout in
|
||||
NavigationLink(destination: WorkoutLogView(workout: workout)) {
|
||||
NavigationLink(destination: WorkoutLogListView(workout: workout)) {
|
||||
CalendarListItem(
|
||||
date: workout.start,
|
||||
title: workout.split?.name ?? Split.unnamed,
|
||||
subtitle: workout.label
|
||||
subtitle: "\(workout.status == .completed ? workout.start.humanTimeInterval(to: (workout.end ?? Date())) : "\(workout.start.formattedDate()) - \(workout.status?.name ?? WorkoutStatus.unnamed)" )",
|
||||
subtitle2: "\(workout.status?.name ?? WorkoutStatus.unnamed)"
|
||||
)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
@ -59,16 +60,9 @@ struct WorkoutsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Workouts")
|
||||
// .toolbar {
|
||||
// ToolbarItem(placement: .primaryAction) {
|
||||
// Button("Start Workout") {
|
||||
// showingSplitPicker = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .sheet(item: $itemToEdit) { item in
|
||||
// WorkoutEditView(workout: item)
|
||||
// }
|
||||
.sheet(item: $itemToEdit) { item in
|
||||
WorkoutEditView(workout: item)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete?",
|
||||
isPresented: Binding<Bool>(
|
||||
@ -94,18 +88,18 @@ struct WorkoutsView: View {
|
||||
}
|
||||
.sheet(isPresented: $showingSplitPicker) {
|
||||
SplitPickerView { split in
|
||||
let workout = Workout(start: Date(), split: split)
|
||||
let workout = Workout(start: Date(), end: Date(), split: split)
|
||||
modelContext.insert(workout)
|
||||
if let exercises = split.exercises {
|
||||
for assignment in exercises {
|
||||
for exercise in exercises {
|
||||
let workoutLog = WorkoutLog(
|
||||
workout: workout,
|
||||
exerciseName: assignment.exerciseName,
|
||||
exerciseName: exercise.name,
|
||||
date: Date(),
|
||||
order: assignment.order,
|
||||
sets: assignment.sets,
|
||||
reps: assignment.reps,
|
||||
weight: assignment.weight
|
||||
order: exercise.order,
|
||||
sets: exercise.sets,
|
||||
reps: exercise.reps,
|
||||
weight: exercise.weight
|
||||
)
|
||||
modelContext.insert(workoutLog)
|
||||
}
|
Reference in New Issue
Block a user