Add CoreData-based workout tracking app with iOS and watchOS targets
- Migrate from SwiftData to CoreData with CloudKit sync - Add core models: Split, Exercise, Workout, WorkoutLog - Implement tab-based UI: Workout Logs, Splits, Settings - Add SF Symbols picker for split icons - Add exercise picker filtered by split with exclusion of added exercises - Integrate IndieAbout for settings/about section - Add Yams for YAML exercise definition parsing - Include starter exercise libraries (bodyweight, Planet Fitness) - Add Date extensions for formatting (formattedTime, isSameDay) - Format workout date ranges to show time-only for same-day end dates - Add build number update script - Add app icons
This commit is contained in:
187
Workouts/Views/Exercises/ExerciseAddEditView.swift
Normal file
187
Workouts/Views/Exercises/ExerciseAddEditView.swift
Normal file
@@ -0,0 +1,187 @@
|
||||
//
|
||||
// ExerciseAddEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Created by rzen on 7/15/25 at 7:12 AM.
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct ExerciseAddEditView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var showingExercisePicker = false
|
||||
|
||||
@ObservedObject var exercise: Exercise
|
||||
|
||||
@State private var originalWeight: Int32? = nil
|
||||
@State private var loadType: LoadType = .none
|
||||
|
||||
@State private var minutes = 0
|
||||
@State private var seconds = 0
|
||||
|
||||
@State private var weight_tens = 0
|
||||
@State private var weight = 0
|
||||
|
||||
@State private var reps: Int = 0
|
||||
@State private var sets: Int = 0
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section(header: Text("Exercise")) {
|
||||
if exercise.name.isEmpty {
|
||||
Button(action: {
|
||||
showingExercisePicker = true
|
||||
}) {
|
||||
HStack {
|
||||
Text("Select Exercise")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ListItem(title: exercise.name)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Sets/Reps")) {
|
||||
HStack(alignment: .bottom) {
|
||||
VStack(alignment: .center) {
|
||||
Text("Sets")
|
||||
Picker("", selection: $sets) {
|
||||
ForEach(0..<20) { s in
|
||||
Text("\(s)").tag(s)
|
||||
}
|
||||
}
|
||||
.frame(height: 100)
|
||||
.pickerStyle(.wheel)
|
||||
}
|
||||
VStack(alignment: .center) {
|
||||
Text("Reps")
|
||||
Picker("", selection: $reps) {
|
||||
ForEach(0..<100) { r in
|
||||
Text("\(r)").tag(r)
|
||||
}
|
||||
}
|
||||
.frame(height: 100)
|
||||
.pickerStyle(.wheel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Load Type"), footer: Text("For bodyweight exercises choose None. For resistance or weight training select Weight. For exercises that are time oriented (like plank or meditation) select Time.")) {
|
||||
Picker("", selection: $loadType) {
|
||||
ForEach(LoadType.allCases, id: \.self) { load in
|
||||
Text(load.name)
|
||||
.tag(load)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
if loadType == .weight {
|
||||
Section(header: Text("Weight")) {
|
||||
HStack {
|
||||
Picker("", selection: $weight_tens) {
|
||||
ForEach(0..<100) { lbs in
|
||||
Text("\(lbs * 10)").tag(lbs * 10)
|
||||
}
|
||||
}
|
||||
.frame(height: 100)
|
||||
.pickerStyle(.wheel)
|
||||
|
||||
Picker("", selection: $weight) {
|
||||
ForEach(0..<10) { lbs in
|
||||
Text("\(lbs)").tag(lbs)
|
||||
}
|
||||
}
|
||||
.frame(height: 100)
|
||||
.pickerStyle(.wheel)
|
||||
|
||||
Text("lbs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if loadType == .duration {
|
||||
Section(header: Text("Duration")) {
|
||||
HStack {
|
||||
Picker("Minutes", selection: $minutes) {
|
||||
ForEach(0..<60) { minute in
|
||||
Text("\(minute) min").tag(minute)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
|
||||
Picker("Seconds", selection: $seconds) {
|
||||
ForEach(0..<60) { second in
|
||||
Text("\(second) sec").tag(second)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Weight Increase")) {
|
||||
HStack {
|
||||
Text("Remind every \(exercise.weightReminderTimeIntervalWeeks) weeks")
|
||||
Spacer()
|
||||
Stepper("", value: Binding(
|
||||
get: { Int(exercise.weightReminderTimeIntervalWeeks) },
|
||||
set: { exercise.weightReminderTimeIntervalWeeks = Int32($0) }
|
||||
), in: 0...366)
|
||||
}
|
||||
if let lastUpdated = exercise.weightLastUpdated {
|
||||
Text("Last weight change \(Date().humanTimeInterval(to: lastUpdated)) ago")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
originalWeight = exercise.weight
|
||||
weight_tens = Int(exercise.weight) / 10 * 10
|
||||
weight = Int(exercise.weight) % 10
|
||||
loadType = exercise.loadTypeEnum
|
||||
sets = Int(exercise.sets)
|
||||
reps = Int(exercise.reps)
|
||||
if let duration = exercise.duration {
|
||||
minutes = Int(duration.timeIntervalSince1970) / 60
|
||||
seconds = Int(duration.timeIntervalSince1970) % 60
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingExercisePicker) {
|
||||
ExercisePickerView { exerciseNames in
|
||||
exercise.name = exerciseNames.first ?? "Unnamed"
|
||||
}
|
||||
}
|
||||
.navigationTitle(exercise.name.isEmpty ? "New Exercise" : exercise.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
if let originalWeight = originalWeight, originalWeight != Int32(weight_tens + weight) {
|
||||
exercise.weightLastUpdated = Date()
|
||||
}
|
||||
exercise.duration = Date(timeIntervalSince1970: Double(minutes * 60 + seconds))
|
||||
exercise.weight = Int32(weight_tens + weight)
|
||||
exercise.sets = Int32(sets)
|
||||
exercise.reps = Int32(reps)
|
||||
exercise.loadType = Int32(loadType.rawValue)
|
||||
try? viewContext.save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user