Refactor UI: move Splits to Settings, redesign ExerciseView
Schema & Models: - Add notes, loadType, duration fields to WorkoutLog - Align Watch schema with iOS (use duration Date instead of separate mins/secs) - Add duration helper properties to Exercise and WorkoutLog UI Changes: - Remove Splits and Settings tabs, single Workout Logs view - Add gear button in nav bar to access Settings as sheet - Move Splits section into Settings view with inline list - Redesign ExerciseView with read-only Plan/Notes tiles and Edit buttons - Add PlanEditView and NotesEditView with Cancel/Save buttons - Auto-dismiss ExerciseView when completing last set - Navigate to ExerciseView when adding new exercise Data Flow: - Plan edits sync to both WorkoutLog and corresponding Exercise - Changes propagate up navigation chain via CoreData
This commit is contained in:
@@ -15,22 +15,7 @@ struct ContentView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
WorkoutLogsView()
|
||||
.tabItem {
|
||||
Label("Workout Logs", systemImage: "list.bullet.clipboard")
|
||||
}
|
||||
|
||||
SplitsView()
|
||||
.tabItem {
|
||||
Label("Splits", systemImage: "dumbbell.fill")
|
||||
}
|
||||
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
WorkoutLogsView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,29 @@ public class Exercise: NSManagedObject, Identifiable {
|
||||
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||
set { loadType = Int32(newValue.rawValue) }
|
||||
}
|
||||
|
||||
// Duration helpers for minutes/seconds conversion
|
||||
var durationMinutes: Int {
|
||||
get {
|
||||
guard let duration = duration else { return 0 }
|
||||
return Int(duration.timeIntervalSince1970) / 60
|
||||
}
|
||||
set {
|
||||
let seconds = durationSeconds
|
||||
duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds))
|
||||
}
|
||||
}
|
||||
|
||||
var durationSeconds: Int {
|
||||
get {
|
||||
guard let duration = duration else { return 0 }
|
||||
return Int(duration.timeIntervalSince1970) % 60
|
||||
}
|
||||
set {
|
||||
let minutes = durationMinutes
|
||||
duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Request
|
||||
|
||||
@@ -12,6 +12,9 @@ public class WorkoutLog: NSManagedObject, Identifiable {
|
||||
@NSManaged public var currentStateIndex: Int32
|
||||
@NSManaged public var elapsedSeconds: Int32
|
||||
@NSManaged public var completed: Bool
|
||||
@NSManaged public var loadType: Int32
|
||||
@NSManaged public var duration: Date?
|
||||
@NSManaged public var notes: String?
|
||||
|
||||
@NSManaged public var workout: Workout?
|
||||
|
||||
@@ -30,6 +33,34 @@ public class WorkoutLog: NSManagedObject, Identifiable {
|
||||
didChangeValue(forKey: "status")
|
||||
}
|
||||
}
|
||||
|
||||
var loadTypeEnum: LoadType {
|
||||
get { LoadType(rawValue: Int(loadType)) ?? .weight }
|
||||
set { loadType = Int32(newValue.rawValue) }
|
||||
}
|
||||
|
||||
// Duration helpers for minutes/seconds conversion
|
||||
var durationMinutes: Int {
|
||||
get {
|
||||
guard let duration = duration else { return 0 }
|
||||
return Int(duration.timeIntervalSince1970) / 60
|
||||
}
|
||||
set {
|
||||
let seconds = durationSeconds
|
||||
duration = Date(timeIntervalSince1970: TimeInterval(newValue * 60 + seconds))
|
||||
}
|
||||
}
|
||||
|
||||
var durationSeconds: Int {
|
||||
get {
|
||||
guard let duration = duration else { return 0 }
|
||||
return Int(duration.timeIntervalSince1970) % 60
|
||||
}
|
||||
set {
|
||||
let minutes = durationMinutes
|
||||
duration = Date(timeIntervalSince1970: TimeInterval(minutes * 60 + newValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch Request
|
||||
|
||||
@@ -6,17 +6,82 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import IndieAbout
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@FetchRequest(
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \Split.order, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Split.name, ascending: true)
|
||||
],
|
||||
animation: .default
|
||||
)
|
||||
private var splits: FetchedResults<Split>
|
||||
|
||||
@State private var showingAddSplitSheet = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// MARK: - Splits Section
|
||||
Section(header: Text("Splits")) {
|
||||
if splits.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "dumbbell.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No Splits Yet")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Text("Create a split to organize your workout routine.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.vertical)
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
ForEach(splits, id: \.objectID) { split in
|
||||
NavigationLink {
|
||||
SplitDetailView(split: split)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: split.systemImage)
|
||||
.foregroundColor(Color.color(from: split.color))
|
||||
.frame(width: 24)
|
||||
Text(split.name)
|
||||
Spacer()
|
||||
Text("\(split.exercisesArray.count)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
showingAddSplitSheet = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
Text("Add Split")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Account Section
|
||||
Section(header: Text("Account")) {
|
||||
Text("Settings coming soon")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// MARK: - About Section
|
||||
Section {
|
||||
IndieAbout(configuration: AppInfoConfiguration(
|
||||
documents: [
|
||||
@@ -28,10 +93,14 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.sheet(isPresented: $showingAddSplitSheet) {
|
||||
SplitAddEditView(split: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
|
||||
}
|
||||
|
||||
@@ -17,46 +17,19 @@ struct ExerciseView: View {
|
||||
|
||||
@ObservedObject var workoutLog: WorkoutLog
|
||||
|
||||
var allLogs: [WorkoutLog]
|
||||
var currentIndex: Int = 0
|
||||
|
||||
@State private var progress: Int = 0
|
||||
@State private var navigateTo: WorkoutLog? = nil
|
||||
@State private var showingPlanEdit = false
|
||||
@State private var showingNotesEdit = false
|
||||
|
||||
let notStartedColor = Color.white
|
||||
let completedColor = Color.green
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Navigation")) {
|
||||
HStack {
|
||||
Button(action: navigateToPrevious) {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Previous")
|
||||
}
|
||||
}
|
||||
.disabled(currentIndex <= 0)
|
||||
|
||||
Spacer()
|
||||
Text("\(currentIndex + 1) of \(allLogs.count)")
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
|
||||
Button(action: navigateToNext) {
|
||||
HStack {
|
||||
Text("Next")
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
.disabled(currentIndex >= allLogs.count - 1)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
// MARK: - Progress Section
|
||||
Section(header: Text("Progress")) {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: Int(workoutLog.sets)), spacing: 2) {
|
||||
ForEach(1...Int(workoutLog.sets), id: \.self) { index in
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: Int(workoutLog.sets)), spacing: 4) {
|
||||
ForEach(1...max(1, Int(workoutLog.sets)), id: \.self) { index in
|
||||
ZStack {
|
||||
let completed = index <= progress
|
||||
let color = completed ? completedColor : notStartedColor
|
||||
@@ -71,95 +44,181 @@ struct ExerciseView: View {
|
||||
.aspectRatio(0.618, contentMode: .fit)
|
||||
.shadow(radius: 2)
|
||||
Text("\(index)")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
.colorInvert()
|
||||
}
|
||||
.onTapGesture {
|
||||
if progress == index {
|
||||
progress = 0
|
||||
} else {
|
||||
progress = index
|
||||
let totalSets = Int(workoutLog.sets)
|
||||
let isLastTile = index == totalSets
|
||||
let wasAlreadyAtThisProgress = progress == index
|
||||
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
if wasAlreadyAtThisProgress {
|
||||
progress = 0
|
||||
} else {
|
||||
progress = index
|
||||
}
|
||||
}
|
||||
|
||||
updateLogStatus()
|
||||
|
||||
// If tapping the last tile to complete, go back to list
|
||||
if isLastTile && !wasAlreadyAtThisProgress {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Plan")) {
|
||||
Stepper("\(workoutLog.sets) sets", value: Binding(
|
||||
get: { Int(workoutLog.sets) },
|
||||
set: { workoutLog.sets = Int32($0) }
|
||||
), in: 1...10)
|
||||
.font(.title)
|
||||
|
||||
Stepper("\(workoutLog.reps) reps", value: Binding(
|
||||
get: { Int(workoutLog.reps) },
|
||||
set: { workoutLog.reps = Int32($0) }
|
||||
), in: 1...25)
|
||||
.font(.title)
|
||||
|
||||
// MARK: - Plan Section (Read-only with Edit button)
|
||||
Section {
|
||||
PlanTilesView(workoutLog: workoutLog)
|
||||
} header: {
|
||||
HStack {
|
||||
Text("\(workoutLog.weight) lbs")
|
||||
VStack(alignment: .trailing) {
|
||||
Stepper("", value: Binding(
|
||||
get: { Int(workoutLog.weight) },
|
||||
set: { workoutLog.weight = Int32($0) }
|
||||
), in: 1...500)
|
||||
Stepper("", value: Binding(
|
||||
get: { Int(workoutLog.weight) },
|
||||
set: { workoutLog.weight = Int32($0) }
|
||||
), in: 1...500, step: 5)
|
||||
Text("Plan")
|
||||
Spacer()
|
||||
Button("Edit") {
|
||||
showingPlanEdit = true
|
||||
}
|
||||
.font(.subheadline)
|
||||
.textCase(.none)
|
||||
}
|
||||
.font(.title)
|
||||
}
|
||||
|
||||
// MARK: - Notes Section (Read-only with Edit button)
|
||||
Section {
|
||||
if let notes = workoutLog.notes, !notes.isEmpty {
|
||||
Text(notes)
|
||||
.foregroundColor(.primary)
|
||||
} else {
|
||||
Text("No notes")
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text("Notes")
|
||||
Spacer()
|
||||
Button("Edit") {
|
||||
showingNotesEdit = true
|
||||
}
|
||||
.font(.subheadline)
|
||||
.textCase(.none)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Progress Tracking Chart
|
||||
Section(header: Text("Progress Tracking")) {
|
||||
WeightProgressionChartView(exerciseName: workoutLog.exerciseName)
|
||||
}
|
||||
}
|
||||
.navigationTitle(workoutLog.exerciseName)
|
||||
.navigationDestination(item: $navigateTo) { nextLog in
|
||||
ExerciseView(
|
||||
workoutLog: nextLog,
|
||||
allLogs: allLogs,
|
||||
currentIndex: allLogs.firstIndex(where: { $0.objectID == nextLog.objectID }) ?? 0
|
||||
)
|
||||
.sheet(isPresented: $showingPlanEdit) {
|
||||
PlanEditView(workoutLog: workoutLog)
|
||||
}
|
||||
.sheet(isPresented: $showingNotesEdit) {
|
||||
NotesEditView(workoutLog: workoutLog)
|
||||
}
|
||||
.onAppear {
|
||||
progress = Int(workoutLog.currentStateIndex)
|
||||
}
|
||||
.onDisappear {
|
||||
saveChanges()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateLogStatus() {
|
||||
workoutLog.currentStateIndex = Int32(progress)
|
||||
if progress >= Int(workoutLog.sets) {
|
||||
workoutLog.status = .completed
|
||||
workoutLog.completed = true
|
||||
} else if progress > 0 {
|
||||
workoutLog.status = .inProgress
|
||||
workoutLog.completed = false
|
||||
} else {
|
||||
workoutLog.status = .notStarted
|
||||
workoutLog.completed = false
|
||||
}
|
||||
updateWorkoutStatus()
|
||||
saveChanges()
|
||||
}
|
||||
|
||||
private func updateWorkoutStatus() {
|
||||
guard let workout = workoutLog.workout else { return }
|
||||
let logs = workout.logsArray
|
||||
let allCompleted = logs.allSatisfy { $0.status == .completed }
|
||||
let anyInProgress = logs.contains { $0.status == .inProgress }
|
||||
let allNotStarted = logs.allSatisfy { $0.status == .notStarted }
|
||||
|
||||
if allCompleted {
|
||||
workout.status = .completed
|
||||
workout.end = Date()
|
||||
} else if anyInProgress || !allNotStarted {
|
||||
workout.status = .inProgress
|
||||
} else {
|
||||
workout.status = .notStarted
|
||||
}
|
||||
}
|
||||
|
||||
private func saveChanges() {
|
||||
try? viewContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToPrevious() {
|
||||
guard currentIndex > 0 else { return }
|
||||
let previousIndex = currentIndex - 1
|
||||
navigateTo = allLogs[previousIndex]
|
||||
// MARK: - Plan Tiles View
|
||||
|
||||
struct PlanTilesView: View {
|
||||
@ObservedObject var workoutLog: WorkoutLog
|
||||
|
||||
var body: some View {
|
||||
if workoutLog.loadTypeEnum == .duration {
|
||||
// Duration layout: Sets | Duration
|
||||
HStack(spacing: 0) {
|
||||
PlanTile(label: "Sets", value: "\(workoutLog.sets)")
|
||||
PlanTile(label: "Duration", value: formattedDuration)
|
||||
}
|
||||
} else {
|
||||
// Weight layout: Sets | Reps | Weight
|
||||
HStack(spacing: 0) {
|
||||
PlanTile(label: "Sets", value: "\(workoutLog.sets)")
|
||||
PlanTile(label: "Reps", value: "\(workoutLog.reps)")
|
||||
PlanTile(label: "Weight", value: "\(workoutLog.weight) lbs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func navigateToNext() {
|
||||
guard currentIndex < allLogs.count - 1 else { return }
|
||||
let nextIndex = currentIndex + 1
|
||||
navigateTo = allLogs[nextIndex]
|
||||
private var formattedDuration: String {
|
||||
let mins = workoutLog.durationMinutes
|
||||
let secs = workoutLog.durationSeconds
|
||||
if mins > 0 && secs > 0 {
|
||||
return "\(mins)m \(secs)s"
|
||||
} else if mins > 0 {
|
||||
return "\(mins) min"
|
||||
} else if secs > 0 {
|
||||
return "\(secs) sec"
|
||||
} else {
|
||||
return "0 sec"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PlanTile: View {
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(value)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
52
Workouts/Views/WorkoutLogs/NotesEditView.swift
Normal file
52
Workouts/Views/WorkoutLogs/NotesEditView.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// NotesEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct NotesEditView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@ObservedObject var workoutLog: WorkoutLog
|
||||
|
||||
@State private var notesText: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
TextEditor(text: $notesText)
|
||||
.frame(minHeight: 200)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Edit Notes")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
saveChanges()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
notesText = workoutLog.notes ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveChanges() {
|
||||
workoutLog.notes = notesText
|
||||
try? viewContext.save()
|
||||
}
|
||||
}
|
||||
167
Workouts/Views/WorkoutLogs/PlanEditView.swift
Normal file
167
Workouts/Views/WorkoutLogs/PlanEditView.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// PlanEditView.swift
|
||||
// Workouts
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct PlanEditView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@ObservedObject var workoutLog: WorkoutLog
|
||||
|
||||
@State private var sets: Int = 3
|
||||
@State private var reps: Int = 12
|
||||
@State private var weight: Int = 0
|
||||
@State private var durationMinutes: Int = 0
|
||||
@State private var durationSeconds: Int = 0
|
||||
@State private var selectedLoadType: LoadType = .weight
|
||||
|
||||
// Find the corresponding exercise in the split for syncing changes
|
||||
private var correspondingExercise: Exercise? {
|
||||
workoutLog.workout?.split?.exercisesArray.first { $0.name == workoutLog.exerciseName }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// Sets and Reps side by side
|
||||
Section {
|
||||
HStack(spacing: 20) {
|
||||
VStack {
|
||||
Text("Sets")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Picker("Sets", selection: $sets) {
|
||||
ForEach(1...7, id: \.self) { num in
|
||||
Text("\(num)").tag(num)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 120)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
VStack {
|
||||
Text("Reps")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Picker("Reps", selection: $reps) {
|
||||
ForEach(1...40, id: \.self) { num in
|
||||
Text("\(num)").tag(num)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 120)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// Load Type Picker
|
||||
Section {
|
||||
Picker("Load Type", selection: $selectedLoadType) {
|
||||
Text("Weight").tag(LoadType.weight)
|
||||
Text("Time").tag(LoadType.duration)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
// Weight or Time picker based on load type
|
||||
Section {
|
||||
if selectedLoadType == .weight {
|
||||
VStack {
|
||||
Text("Weight")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Picker("Weight", selection: $weight) {
|
||||
ForEach(0...300, id: \.self) { num in
|
||||
Text("\(num) lbs").tag(num)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 150)
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 20) {
|
||||
VStack {
|
||||
Text("Mins")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Picker("Minutes", selection: $durationMinutes) {
|
||||
ForEach(0...60, id: \.self) { num in
|
||||
Text("\(num)").tag(num)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 120)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
VStack {
|
||||
Text("Secs")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Picker("Seconds", selection: $durationSeconds) {
|
||||
ForEach(0...59, id: \.self) { num in
|
||||
Text("\(num)").tag(num)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.wheel)
|
||||
.frame(height: 120)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Edit Plan")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
saveChanges()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
sets = Int(workoutLog.sets)
|
||||
reps = Int(workoutLog.reps)
|
||||
weight = Int(workoutLog.weight)
|
||||
durationMinutes = workoutLog.durationMinutes
|
||||
durationSeconds = workoutLog.durationSeconds
|
||||
selectedLoadType = workoutLog.loadTypeEnum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveChanges() {
|
||||
workoutLog.sets = Int32(sets)
|
||||
workoutLog.reps = Int32(reps)
|
||||
workoutLog.weight = Int32(weight)
|
||||
workoutLog.durationMinutes = durationMinutes
|
||||
workoutLog.durationSeconds = durationSeconds
|
||||
workoutLog.loadTypeEnum = selectedLoadType
|
||||
|
||||
// Sync to corresponding exercise
|
||||
if let exercise = correspondingExercise {
|
||||
exercise.sets = workoutLog.sets
|
||||
exercise.reps = workoutLog.reps
|
||||
exercise.weight = workoutLog.weight
|
||||
exercise.loadType = workoutLog.loadType
|
||||
exercise.duration = workoutLog.duration
|
||||
}
|
||||
|
||||
try? viewContext.save()
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ struct WorkoutLogListView: View {
|
||||
|
||||
@State private var showingAddSheet = false
|
||||
@State private var itemToDelete: WorkoutLog? = nil
|
||||
@State private var newlyAddedLog: WorkoutLog? = nil
|
||||
|
||||
var sortedWorkoutLogs: [WorkoutLog] {
|
||||
workout.logsArray
|
||||
@@ -40,20 +41,16 @@ struct WorkoutLogListView: View {
|
||||
} else {
|
||||
Form {
|
||||
Section(header: Text("\(workout.label)")) {
|
||||
ForEach(Array(sortedWorkoutLogs.enumerated()), id: \.element.objectID) { index, log in
|
||||
ForEach(sortedWorkoutLogs, id: \.objectID) { log in
|
||||
let workoutLogStatus = log.status.checkboxStatus
|
||||
|
||||
NavigationLink {
|
||||
ExerciseView(
|
||||
workoutLog: log,
|
||||
allLogs: sortedWorkoutLogs,
|
||||
currentIndex: index
|
||||
)
|
||||
ExerciseView(workoutLog: log)
|
||||
} label: {
|
||||
CheckboxListItem(
|
||||
status: workoutLogStatus,
|
||||
title: log.exerciseName,
|
||||
subtitle: "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
|
||||
subtitle: subtitleForLog(log)
|
||||
) {
|
||||
cycleStatus(for: log)
|
||||
}
|
||||
@@ -80,6 +77,9 @@ struct WorkoutLogListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $newlyAddedLog) { log in
|
||||
ExerciseView(workoutLog: log)
|
||||
}
|
||||
.navigationTitle("\(workout.split?.name ?? Split.unnamed)")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -182,10 +182,31 @@ struct WorkoutLogListView: View {
|
||||
log.sets = exercise.sets
|
||||
log.reps = exercise.reps
|
||||
log.weight = exercise.weight
|
||||
log.loadType = exercise.loadType
|
||||
log.duration = exercise.duration
|
||||
log.status = .notStarted
|
||||
log.workout = workout
|
||||
|
||||
try? viewContext.save()
|
||||
|
||||
// Navigate to the new exercise view
|
||||
newlyAddedLog = log
|
||||
}
|
||||
|
||||
private func subtitleForLog(_ log: WorkoutLog) -> String {
|
||||
if log.loadTypeEnum == .duration {
|
||||
let mins = log.durationMinutes
|
||||
let secs = log.durationSeconds
|
||||
if mins > 0 && secs > 0 {
|
||||
return "\(log.sets) × \(mins)m \(secs)s"
|
||||
} else if mins > 0 {
|
||||
return "\(log.sets) × \(mins) min"
|
||||
} else {
|
||||
return "\(log.sets) × \(secs) sec"
|
||||
}
|
||||
} else {
|
||||
return "\(log.sets) × \(log.reps) reps × \(log.weight) lbs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ struct WorkoutLogsView: View {
|
||||
private var workouts: FetchedResults<Workout>
|
||||
|
||||
@State private var showingSplitPicker = false
|
||||
@State private var showingSettings = false
|
||||
@State private var itemToDelete: Workout? = nil
|
||||
|
||||
var body: some View {
|
||||
@@ -55,12 +56,22 @@ struct WorkoutLogsView: View {
|
||||
}
|
||||
.navigationTitle("Workout Logs")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
showingSettings = true
|
||||
} label: {
|
||||
Image(systemName: "gearshape.2")
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Start New") {
|
||||
showingSplitPicker.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingSettings) {
|
||||
SettingsView()
|
||||
}
|
||||
.sheet(isPresented: $showingSplitPicker) {
|
||||
SplitPickerSheet()
|
||||
}
|
||||
|
||||
@@ -31,8 +31,11 @@
|
||||
<attribute name="completed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="currentStateIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
||||
<attribute name="duration" optional="YES" attributeType="Date" defaultDateTimeInterval="0" usesScalarValueType="NO"/>
|
||||
<attribute name="elapsedSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="exerciseName" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="loadType" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="order" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="reps" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sets" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
||||
Reference in New Issue
Block a user