diff --git a/WorkoutEditor.xcodeproj/project.pbxproj b/WorkoutEditor.xcodeproj/project.pbxproj index 4ea8140..6f6e411 100644 --- a/WorkoutEditor.xcodeproj/project.pbxproj +++ b/WorkoutEditor.xcodeproj/project.pbxproj @@ -8,20 +8,22 @@ /* Begin PBXBuildFile section */ 06E7DFAC2B3608DD0025260F /* WorkoutEditorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */; }; - 06E7DFAE2B3608DD0025260F /* WorkoutAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */; }; 06E7DFB02B3608E00025260F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 06E7DFAF2B3608E00025260F /* Assets.xcassets */; }; 06E7DFB32B3608E00025260F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 06E7DFB22B3608E00025260F /* Preview Assets.xcassets */; }; 06E7DFC32B3653F70025260F /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFC22B3653F70025260F /* HealthKitManager.swift */; }; 06E7DFC52B3654500025260F /* WorkoutListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFC42B3654500025260F /* WorkoutListView.swift */; }; 06E7DFC72B3654D50025260F /* WorkoutEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */; }; + AA000001AAAA000100000001 /* ActivityGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000001AAAA000100000002 /* ActivityGraphView.swift */; }; + AA000002AAAA000200000001 /* RangeSliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002AAAA000200000002 /* RangeSliderView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 063E686B2B45161B0048778C /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; 06E7DFA82B3608DD0025260F /* WorkoutEditor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WorkoutEditor.app; sourceTree = BUILT_PRODUCTS_DIR; }; 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEditorApp.swift; sourceTree = ""; }; - 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutAddView.swift; sourceTree = ""; }; 06E7DFAF2B3608E00025260F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AA000001AAAA000100000002 /* ActivityGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityGraphView.swift; sourceTree = ""; }; + AA000002AAAA000200000002 /* RangeSliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RangeSliderView.swift; sourceTree = ""; }; 06E7DFB22B3608E00025260F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 06E7DFB92B3609EA0025260F /* WorkoutEditor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WorkoutEditor.entitlements; sourceTree = ""; }; 06E7DFC22B3653F70025260F /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = ""; }; @@ -72,8 +74,9 @@ 06E7DFC82B3668D10025260F /* Info.plist */, 06E7DFB92B3609EA0025260F /* WorkoutEditor.entitlements */, 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */, - 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */, 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */, + AA000001AAAA000100000002 /* ActivityGraphView.swift */, + AA000002AAAA000200000002 /* RangeSliderView.swift */, 06E7DFC42B3654500025260F /* WorkoutListView.swift */, 06E7DFC22B3653F70025260F /* HealthKitManager.swift */, 06E7DFAF2B3608E00025260F /* Assets.xcassets */, @@ -160,11 +163,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 06E7DFAE2B3608DD0025260F /* WorkoutAddView.swift in Sources */, 06E7DFAC2B3608DD0025260F /* WorkoutEditorApp.swift in Sources */, 06E7DFC52B3654500025260F /* WorkoutListView.swift in Sources */, 06E7DFC32B3653F70025260F /* HealthKitManager.swift in Sources */, 06E7DFC72B3654D50025260F /* WorkoutEditView.swift in Sources */, + AA000001AAAA000100000001 /* ActivityGraphView.swift in Sources */, + AA000002AAAA000200000001 /* RangeSliderView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/WorkoutEditor/ActivityGraphView.swift b/WorkoutEditor/ActivityGraphView.swift new file mode 100644 index 0000000..d3b4d3a --- /dev/null +++ b/WorkoutEditor/ActivityGraphView.swift @@ -0,0 +1,71 @@ +import SwiftUI +import Charts + +struct ActivityGraphView: View { + let samples: [IntensitySample] + let metric: IntensityMetric + let lowerBound: Double + let upperBound: Double + + var body: some View { + Chart { + ForEach(samples) { sample in + AreaMark( + x: .value("Time", sample.date), + y: .value(metric.label, sample.value) + ) + .foregroundStyle( + .linearGradient( + colors: [.blue.opacity(0.3), .blue.opacity(0.05)], + startPoint: .top, + endPoint: .bottom + ) + ) + + LineMark( + x: .value("Time", sample.date), + y: .value(metric.label, sample.value) + ) + .foregroundStyle(.blue) + .lineStyle(StrokeStyle(lineWidth: 1.5)) + } + + // Dimming overlay for trimmed start region + if lowerBound > 0, let startDate = samples.first?.date, let endDate = samples.last?.date { + let trimStart = startDate.addingTimeInterval( + lowerBound * endDate.timeIntervalSince(startDate) + ) + RectangleMark( + xStart: .value("", startDate), + xEnd: .value("", trimStart) + ) + .foregroundStyle(Color(.systemBackground).opacity(0.7)) + } + + // Dimming overlay for trimmed end region + if upperBound < 1, let startDate = samples.first?.date, let endDate = samples.last?.date { + let trimEnd = startDate.addingTimeInterval( + upperBound * endDate.timeIntervalSince(startDate) + ) + RectangleMark( + xStart: .value("", trimEnd), + xEnd: .value("", endDate) + ) + .foregroundStyle(Color(.systemBackground).opacity(0.7)) + } + } + .chartXAxis(.hidden) + .chartYAxis { + AxisMarks(position: .leading, values: .automatic(desiredCount: 3)) { value in + AxisGridLine() + AxisValueLabel { + if let v = value.as(Double.self) { + Text("\(Int(v))") + .font(.caption2) + } + } + } + } + .chartYAxisLabel(metric.label, position: .leading) + } +} diff --git a/WorkoutEditor/HealthKitManager.swift b/WorkoutEditor/HealthKitManager.swift index f2b096a..dfaa09c 100644 --- a/WorkoutEditor/HealthKitManager.swift +++ b/WorkoutEditor/HealthKitManager.swift @@ -1,6 +1,42 @@ import Foundation import HealthKit +struct IntensitySample: Identifiable { + let id = UUID() + let date: Date + let value: Double +} + +enum IntensityMetric { + case heartRate + case activeEnergy + case distance + + var unit: HKUnit { + switch self { + case .heartRate: .count().unitDivided(by: .minute()) + case .activeEnergy: .kilocalorie() + case .distance: .meter() + } + } + + var label: String { + switch self { + case .heartRate: "BPM" + case .activeEnergy: "kcal" + case .distance: "m" + } + } + + var quantityType: HKQuantityType { + switch self { + case .heartRate: HKQuantityType(.heartRate) + case .activeEnergy: HKQuantityType(.activeEnergyBurned) + case .distance: HKQuantityType(.distanceWalkingRunning) + } + } +} + @Observable class HealthKitManager { static let shared = HealthKitManager() @@ -13,8 +49,17 @@ class HealthKitManager { throw HealthKitError.healthDataNotAvailable } - let typesToRead: Set = [HKObjectType.workoutType()] - let typesToWrite: Set = [HKObjectType.workoutType()] + let typesToRead: Set = [ + HKObjectType.workoutType(), + HKQuantityType(.heartRate), + HKQuantityType(.activeEnergyBurned), + HKQuantityType(.distanceWalkingRunning), + HKQuantityType(.distanceCycling), + ] + let typesToWrite: Set = [ + HKObjectType.workoutType(), + HKQuantityType(.heartRate), + ] try await healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) } @@ -37,6 +82,51 @@ class HealthKitManager { } } + func fetchIntensitySamples(for workout: HKWorkout) async -> (samples: [IntensitySample], metric: IntensityMetric) { + let metrics: [IntensityMetric] = [.heartRate, .activeEnergy, .distance] + + for metric in metrics { + let predicate = HKQuery.predicateForSamples( + withStart: workout.startDate, + end: workout.endDate, + options: .strictStartDate + ) + let sortDescriptor = SortDescriptor(\HKQuantitySample.startDate, order: .forward) + let descriptor = HKSampleQueryDescriptor( + predicates: [.quantitySample(type: metric.quantityType, predicate: predicate)], + sortDescriptors: [sortDescriptor], + limit: 5000 + ) + + do { + let results = try await descriptor.result(for: healthStore) + if !results.isEmpty { + var samples = results.map { sample in + IntensitySample( + date: sample.startDate, + value: sample.quantity.doubleValue(for: metric.unit) + ) + } + if samples.count > 500 { + samples = downsample(samples, to: 500) + } + return (samples, metric) + } + } catch { + print("Error fetching \(metric.label): \(error.localizedDescription)") + } + } + + return ([], .heartRate) + } + + private func downsample(_ samples: [IntensitySample], to target: Int) -> [IntensitySample] { + let stride = Double(samples.count) / Double(target) + return (0.. some View { + Circle() + .fill(.white) + .shadow(color: .black.opacity(0.15), radius: 3, y: 1) + .frame(width: thumbSize, height: thumbSize) + .contentShape(Rectangle().size(width: 44, height: 44)) + } +} diff --git a/WorkoutEditor/WorkoutAddView.swift b/WorkoutEditor/WorkoutAddView.swift deleted file mode 100644 index f053c53..0000000 --- a/WorkoutEditor/WorkoutAddView.swift +++ /dev/null @@ -1,115 +0,0 @@ -import SwiftUI -import HealthKit - -struct WorkoutAddView: View { - @Environment(\.dismiss) private var dismiss - - @State private var selectedActivityType: HKWorkoutActivityType = .running - @State private var startTime = Date() - @State private var endTime = Date() - @State private var errorMessage: String? - - private var isValid: Bool { - endTime > startTime - } - - var body: some View { - Form { - Section("New Workout Details") { - DatePicker("Start", selection: $startTime) - DatePicker("End", selection: $endTime) - - Picker("Activity Type", selection: $selectedActivityType) { - ForEach(HKWorkoutActivityType.commonTypes, id: \.self) { type in - Text(type.activityTypeDescription).tag(type) - } - } - } - - if !isValid { - Section { - Label("End time must be after start time.", systemImage: "exclamationmark.triangle") - .foregroundStyle(.red) - } - } - - Section { - Button("Save Workout") { - saveWorkout() - } - .disabled(!isValid) - } - - if let errorMessage { - Section { - Label(errorMessage, systemImage: "xmark.circle") - .foregroundStyle(.red) - } - } - } - .navigationTitle("Add Workout") - .navigationBarTitleDisplayMode(.inline) - } - - private func saveWorkout() { - Task { - do { - let metadata = [HKMetadataKeyTimeZone: TimeZone.current.identifier] - try await HealthKitManager.shared.saveWorkout( - activityType: selectedActivityType, - start: startTime, - end: endTime, - metadata: metadata - ) - await HealthKitManager.shared.loadWorkouts() - dismiss() - } catch { - errorMessage = error.localizedDescription - } - } - } -} - -// MARK: - Activity Type Helpers - -extension HKWorkoutActivityType { - static let commonTypes: [HKWorkoutActivityType] = [ - .running, .walking, .hiking, .cycling, .swimming, - .yoga, .functionalStrengthTraining, .traditionalStrengthTraining, - .crossTraining, .elliptical, .rowing, .stairClimbing, - .highIntensityIntervalTraining, .socialDance, .cooldown, - .coreTraining, .pilates, .kickboxing, .boxing, .climbing - ] - - var activityTypeDescription: String { - switch self { - case .running: "Running" - case .walking: "Walking" - case .hiking: "Hiking" - case .cycling: "Cycling" - case .swimming: "Swimming" - case .yoga: "Yoga" - case .functionalStrengthTraining: "Functional Strength" - case .traditionalStrengthTraining: "Strength Training" - case .crossTraining: "Cross Training" - case .elliptical: "Elliptical" - case .rowing: "Rowing" - case .stairClimbing: "Stair Climbing" - case .highIntensityIntervalTraining: "HIIT" - case .socialDance: "Dance" - case .cooldown: "Cooldown" - case .coreTraining: "Core Training" - case .pilates: "Pilates" - case .kickboxing: "Kickboxing" - case .boxing: "Boxing" - case .climbing: "Climbing" - default: "Other" - } - } -} - -#Preview { - NavigationStack { - WorkoutAddView() - } -} diff --git a/WorkoutEditor/WorkoutEditView.swift b/WorkoutEditor/WorkoutEditView.swift index fd3d978..ba9efaf 100644 --- a/WorkoutEditor/WorkoutEditView.swift +++ b/WorkoutEditor/WorkoutEditView.swift @@ -5,76 +5,179 @@ struct WorkoutEditView: View { let workout: HKWorkout @Environment(\.dismiss) private var dismiss - @State private var editedStartTime: Date - @State private var editedEndTime: Date + @State private var lowerBound: Double = 0 + @State private var upperBound: Double = 1 @State private var editedActivityType: HKWorkoutActivityType + @State private var intensitySamples: [IntensitySample] = [] + @State private var intensityMetric: IntensityMetric = .heartRate + @State private var isLoadingSamples = true @State private var showSaveAlert = false @State private var showDeleteAlert = false @State private var errorMessage: String? init(workout: HKWorkout) { self.workout = workout - _editedStartTime = State(initialValue: workout.startDate) - _editedEndTime = State(initialValue: workout.endDate) _editedActivityType = State(initialValue: workout.workoutActivityType) } + private var workoutDuration: TimeInterval { + workout.endDate.timeIntervalSince(workout.startDate) + } + + private var trimmedStartDate: Date { + workout.startDate.addingTimeInterval(lowerBound * workoutDuration) + } + + private var trimmedEndDate: Date { + workout.startDate.addingTimeInterval(upperBound * workoutDuration) + } + + private var trimmedDuration: TimeInterval { + trimmedEndDate.timeIntervalSince(trimmedStartDate) + } + private var isValid: Bool { - editedEndTime > editedStartTime + trimmedDuration > 0 } var body: some View { - Form { - Section("Workout Details") { - DatePicker("Start", selection: $editedStartTime) - DatePicker("End", selection: $editedEndTime) - - Picker("Activity Type", selection: $editedActivityType) { - ForEach(HKWorkoutActivityType.commonTypes, id: \.self) { type in - Text(type.activityTypeDescription).tag(type) + ScrollView { + VStack(spacing: 20) { + // Activity graph + Group { + if isLoadingSamples { + ProgressView("Loading activity data...") + .frame(height: 200) + } else if intensitySamples.isEmpty { + ContentUnavailableView( + "No Activity Data", + systemImage: "waveform.slash", + description: Text("No heart rate or activity samples found for this workout.") + ) + .frame(height: 200) + } else { + ActivityGraphView( + samples: intensitySamples, + metric: intensityMetric, + lowerBound: lowerBound, + upperBound: upperBound + ) + .frame(height: 200) } } - } + .padding(.horizontal) - if !isValid { - Section { - Label("End time must be after start time.", systemImage: "exclamationmark.triangle") - .foregroundStyle(.red) - } - } + // Range slider + RangeSliderView(lowerBound: $lowerBound, upperBound: $upperBound) + .padding(.horizontal) - Section { - Button("Save Changes") { - showSaveAlert = true - } - .disabled(!isValid) - .alert("Save Changes", isPresented: $showSaveAlert) { - Button("Save") { saveChanges() } - Button("Cancel", role: .cancel) {} - } message: { - Text("Are you sure you want to save these changes?") + // Time labels + HStack { + VStack(alignment: .leading) { + Text("Start") + .font(.caption) + .foregroundStyle(.secondary) + Text(trimmedStartDate.formatted(date: .omitted, time: .shortened)) + .font(.subheadline.monospacedDigit()) + } + + Spacer() + + VStack { + Text("Duration") + .font(.caption) + .foregroundStyle(.secondary) + Text(formattedDuration(trimmedDuration)) + .font(.subheadline.monospacedDigit()) + } + + Spacer() + + VStack(alignment: .trailing) { + Text("End") + .font(.caption) + .foregroundStyle(.secondary) + Text(trimmedEndDate.formatted(date: .omitted, time: .shortened)) + .font(.subheadline.monospacedDigit()) + } } + .padding(.horizontal) - Button("Delete Workout", role: .destructive) { - showDeleteAlert = true + Divider() + .padding(.horizontal) + + // Activity type picker + HStack { + Text("Activity Type") + Spacer() + Picker("Activity Type", selection: $editedActivityType) { + ForEach(HKWorkoutActivityType.commonTypes, id: \.self) { type in + Text(type.activityTypeDescription).tag(type) + } + } + .labelsHidden() } - .alert("Delete Workout", isPresented: $showDeleteAlert) { - Button("Delete", role: .destructive) { deleteWorkout() } - Button("Cancel", role: .cancel) {} - } message: { - Text("This action cannot be undone.") + .padding(.horizontal) + + Divider() + .padding(.horizontal) + + // Action buttons + VStack(spacing: 12) { + Button { + showSaveAlert = true + } label: { + Text("Save Changes") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .disabled(!isValid) + + Button(role: .destructive) { + showDeleteAlert = true + } label: { + Text("Delete Workout") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) } - } + .padding(.horizontal) - if let errorMessage { - Section { + if let errorMessage { Label(errorMessage, systemImage: "xmark.circle") .foregroundStyle(.red) + .padding(.horizontal) } } + .padding(.vertical) } .navigationTitle(workout.workoutActivityType.activityTypeDescription) .navigationBarTitleDisplayMode(.inline) + .task { + let result = await HealthKitManager.shared.fetchIntensitySamples(for: workout) + intensitySamples = result.samples + intensityMetric = result.metric + isLoadingSamples = false + } + .alert("Save Changes", isPresented: $showSaveAlert) { + Button("Save", role: .destructive) { saveChanges() } + Button("Cancel", role: .cancel) {} + } message: { + Text("This will delete the original workout and replace it with the trimmed version.\n\nTHIS ACTION CANNOT BE UNDONE.") + } + .alert("Delete Workout", isPresented: $showDeleteAlert) { + Button("Delete", role: .destructive) { deleteWorkout() } + Button("Cancel", role: .cancel) {} + } message: { + Text("This action cannot be undone.") + } + } + + private func formattedDuration(_ duration: TimeInterval) -> String { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .abbreviated + return formatter.string(from: duration) ?? "" } private func saveChanges() { @@ -82,8 +185,8 @@ struct WorkoutEditView: View { do { try await HealthKitManager.shared.saveWorkout( activityType: editedActivityType, - start: editedStartTime, - end: editedEndTime, + start: trimmedStartDate, + end: trimmedEndDate, metadata: workout.metadata as [String: Any]? ) try await HealthKitManager.shared.deleteWorkout(workout) @@ -108,6 +211,44 @@ struct WorkoutEditView: View { } } +// MARK: - Activity Type Helpers + +extension HKWorkoutActivityType { + static let commonTypes: [HKWorkoutActivityType] = [ + .running, .walking, .hiking, .cycling, .swimming, + .yoga, .functionalStrengthTraining, .traditionalStrengthTraining, + .crossTraining, .elliptical, .rowing, .stairClimbing, + .highIntensityIntervalTraining, .socialDance, .cooldown, + .coreTraining, .pilates, .kickboxing, .boxing, .climbing + ] + + var activityTypeDescription: String { + switch self { + case .running: "Running" + case .walking: "Walking" + case .hiking: "Hiking" + case .cycling: "Cycling" + case .swimming: "Swimming" + case .yoga: "Yoga" + case .functionalStrengthTraining: "Functional Strength" + case .traditionalStrengthTraining: "Strength Training" + case .crossTraining: "Cross Training" + case .elliptical: "Elliptical" + case .rowing: "Rowing" + case .stairClimbing: "Stair Climbing" + case .highIntensityIntervalTraining: "HIIT" + case .socialDance: "Dance" + case .cooldown: "Cooldown" + case .coreTraining: "Core Training" + case .pilates: "Pilates" + case .kickboxing: "Kickboxing" + case .boxing: "Boxing" + case .climbing: "Climbing" + default: "Other" + } + } +} + #Preview { NavigationStack { WorkoutEditView(workout: HKWorkout(activityType: .running, start: Date(), end: Date())) diff --git a/WorkoutEditor/WorkoutEditorApp.swift b/WorkoutEditor/WorkoutEditorApp.swift index e673a7a..b69d1ce 100644 --- a/WorkoutEditor/WorkoutEditorApp.swift +++ b/WorkoutEditor/WorkoutEditorApp.swift @@ -1,4 +1,7 @@ import SwiftUI +import os + +private let logger = Logger(subsystem: "com.workouteditor", category: "app") @main struct WorkoutEditorApp: App { @@ -8,9 +11,20 @@ struct WorkoutEditorApp: App { .task { do { try await HealthKitManager.shared.requestAuthorization() + logger.notice("Auth succeeded") + await HealthKitManager.shared.loadWorkouts() + logger.notice("Loaded \(HealthKitManager.shared.loadedWorkouts.count) workouts") + #if DEBUG + // Reset sample data + for workout in HealthKitManager.shared.loadedWorkouts { + try await HealthKitManager.shared.deleteWorkout(workout) + } + try await HealthKitManager.shared.insertSampleWorkouts() await HealthKitManager.shared.loadWorkouts() + logger.notice("Reset samples: \(HealthKitManager.shared.loadedWorkouts.count) workouts") + #endif } catch { - print("HealthKit authorization error: \(error.localizedDescription)") + logger.error("HealthKit error: \(error)") } } } diff --git a/WorkoutEditor/WorkoutListView.swift b/WorkoutEditor/WorkoutListView.swift index c91db9d..fea54d8 100644 --- a/WorkoutEditor/WorkoutListView.swift +++ b/WorkoutEditor/WorkoutListView.swift @@ -3,6 +3,7 @@ import HealthKit struct WorkoutListView: View { var manager = HealthKitManager.shared + @Environment(\.scenePhase) private var scenePhase var body: some View { NavigationStack { @@ -28,19 +29,14 @@ struct WorkoutListView: View { WorkoutEditView(workout: workout) } .navigationTitle("Workouts") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - NavigationLink(value: "add") { - Image(systemName: "plus") - } - } - } - .navigationDestination(for: String.self) { _ in - WorkoutAddView() - } .refreshable { await manager.loadWorkouts() } + .onChange(of: scenePhase) { _, phase in + if phase == .active { + Task { await manager.loadWorkouts() } + } + } .overlay { if manager.loadedWorkouts.isEmpty { ContentUnavailableView("No Workouts", systemImage: "figure.run", description: Text("Your workouts will appear here."))