From fa79687ec59dd514f89feac9accf1bdb3b42a2ac Mon Sep 17 00:00:00 2001 From: mbenz12 Date: Wed, 3 Jan 2024 14:34:29 +0200 Subject: [PATCH] workout editor implemented basically --- WorkoutEditor.xcodeproj/project.pbxproj | 38 ++++- WorkoutEditor/ContentView.swift | 26 --- WorkoutEditor/HealthKitManager.swift | 87 ++++++++++ WorkoutEditor/Info.plist | 12 ++ WorkoutEditor/WorkoutAddView.swift | 95 +++++++++++ WorkoutEditor/WorkoutEditView.swift | 195 +++++++++++++++++++++++ WorkoutEditor/WorkoutEditor.entitlements | 10 ++ WorkoutEditor/WorkoutEditorApp.swift | 13 +- WorkoutEditor/WorkoutListView.swift | 53 ++++++ 9 files changed, 498 insertions(+), 31 deletions(-) delete mode 100644 WorkoutEditor/ContentView.swift create mode 100644 WorkoutEditor/HealthKitManager.swift create mode 100644 WorkoutEditor/Info.plist create mode 100644 WorkoutEditor/WorkoutAddView.swift create mode 100644 WorkoutEditor/WorkoutEditView.swift create mode 100644 WorkoutEditor/WorkoutEditor.entitlements create mode 100644 WorkoutEditor/WorkoutListView.swift diff --git a/WorkoutEditor.xcodeproj/project.pbxproj b/WorkoutEditor.xcodeproj/project.pbxproj index de47af2..11c17d1 100644 --- a/WorkoutEditor.xcodeproj/project.pbxproj +++ b/WorkoutEditor.xcodeproj/project.pbxproj @@ -8,17 +8,26 @@ /* Begin PBXBuildFile section */ 06E7DFAC2B3608DD0025260F /* WorkoutEditorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */; }; - 06E7DFAE2B3608DD0025260F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAD2B3608DD0025260F /* ContentView.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 */; }; /* 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 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.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 = ""; }; 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 = ""; }; + 06E7DFC42B3654500025260F /* WorkoutListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutListView.swift; sourceTree = ""; }; + 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEditView.swift; sourceTree = ""; }; + 06E7DFC82B3668D10025260F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -32,11 +41,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 063E686A2B45161A0048778C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 063E686B2B45161B0048778C /* HealthKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 06E7DF9F2B3608DD0025260F = { isa = PBXGroup; children = ( 06E7DFAA2B3608DD0025260F /* WorkoutEditor */, 06E7DFA92B3608DD0025260F /* Products */, + 063E686A2B45161A0048778C /* Frameworks */, ); sourceTree = ""; }; @@ -51,8 +69,13 @@ 06E7DFAA2B3608DD0025260F /* WorkoutEditor */ = { isa = PBXGroup; children = ( + 06E7DFC82B3668D10025260F /* Info.plist */, + 06E7DFB92B3609EA0025260F /* WorkoutEditor.entitlements */, 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */, - 06E7DFAD2B3608DD0025260F /* ContentView.swift */, + 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */, + 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */, + 06E7DFC42B3654500025260F /* WorkoutListView.swift */, + 06E7DFC22B3653F70025260F /* HealthKitManager.swift */, 06E7DFAF2B3608E00025260F /* Assets.xcassets */, 06E7DFB12B3608E00025260F /* Preview Content */, ); @@ -137,8 +160,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 06E7DFAE2B3608DD0025260F /* ContentView.swift in Sources */, + 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 */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -264,11 +290,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WorkoutEditor/WorkoutEditor.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"WorkoutEditor/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WorkoutEditor/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -292,11 +320,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WorkoutEditor/WorkoutEditor.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"WorkoutEditor/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WorkoutEditor/Info.plist; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/WorkoutEditor/ContentView.swift b/WorkoutEditor/ContentView.swift deleted file mode 100644 index 66cf19b..0000000 --- a/WorkoutEditor/ContentView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ContentView.swift -// WorkoutEditor -// -// Created by Vladyslav on 22.12.2023. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/WorkoutEditor/HealthKitManager.swift b/WorkoutEditor/HealthKitManager.swift new file mode 100644 index 0000000..f1cce44 --- /dev/null +++ b/WorkoutEditor/HealthKitManager.swift @@ -0,0 +1,87 @@ +// +// HealthKitManager.swift +// WorkoutEditor +// +// Created by Vladyslav on 23.12.2023. +// + +import Foundation +import HealthKit + +class HealthKitManager { + static let shared = HealthKitManager() + + private let healthStore = HKHealthStore() + + // Request authorization for specific health data types + func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) { + guard HKHealthStore.isHealthDataAvailable() else { + completion(false, nil) + return + } + + let typesToRead: Set = [HKObjectType.workoutType()] + + let typesToWrite: Set = [HKObjectType.workoutType()] + + healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { (success, error) in + completion(success, error) + } + } + + func save(_ workout: HKWorkout, completion: @escaping (Bool, Error?) -> Void) { + self.healthStore.save(workout) { success, error in + completion(success, error) + } + } + + func loadWorkouts(completion: @escaping ([HKWorkout]?, Error?) -> Void) { + let workoutType = HKObjectType.workoutType() + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) + let query = HKSampleQuery(sampleType: workoutType, predicate: nil, limit: 100, sortDescriptors: [sortDescriptor]) { (query, samples, error) in + guard let workouts = samples as? [HKWorkout] else { + completion(nil, error) + return + } + completion(workouts, error) + } + + self.healthStore.execute(query) + } + + func fetchWorkout(withUUID uuid: UUID, completion: @escaping (HKWorkout?, Error?) -> Void) { + guard HKHealthStore.isHealthDataAvailable() else { + completion(nil, HealthKitError.healthDataNotAvailable) + return + } + + let workoutType = HKObjectType.workoutType() + let predicate = HKQuery.predicateForObject(with: uuid) + + let query = HKSampleQuery( + sampleType: workoutType, + predicate: predicate, + limit: HKObjectQueryNoLimit, + sortDescriptors: nil + ) { (query, samples, error) in + guard let workouts = samples as? [HKWorkout], let workout = workouts.first else { + completion(nil, error) + return + } + completion(workout, nil) + } + + healthStore.execute(query) + } + + func deleteWorkout(_ workout: HKWorkout, completion: @escaping (Bool, Error?) -> Void) { + healthStore.delete(workout) { success, error in + completion(success, error) + } + } +} + +enum HealthKitError: Error { + case healthDataNotAvailable + // Add more error cases as needed +} diff --git a/WorkoutEditor/Info.plist b/WorkoutEditor/Info.plist new file mode 100644 index 0000000..f713856 --- /dev/null +++ b/WorkoutEditor/Info.plist @@ -0,0 +1,12 @@ + + + + + UIBackgroundModes + + NSHealthShareUsageDescription + We need to read data + NSHealthUpdateUsageDescription + We need to write data + + diff --git a/WorkoutEditor/WorkoutAddView.swift b/WorkoutEditor/WorkoutAddView.swift new file mode 100644 index 0000000..614ab14 --- /dev/null +++ b/WorkoutEditor/WorkoutAddView.swift @@ -0,0 +1,95 @@ +// +// ContentView.swift +// WorkoutEditor +// +// Created by Vladyslav on 22.12.2023. +// + +import SwiftUI +import HealthKit + +struct WorkoutAddView: View { + @State private var selectedActivityType: HKWorkoutActivityType = .running + @State private var startTime = Date() + @State private var endTime = Date() + + var activityTypes: [HKWorkoutActivityType] = [ + .running, + .cycling, + .swimming, + // Add more activity types as needed + ] + @State private var isSaveSuccessful: Bool = false + var body: some View { + NavigationView { + Form() { + Section(header: Text("New Workout Details")) { + DatePicker("Start Time", selection: $startTime, displayedComponents: .hourAndMinute) + .datePickerStyle(GraphicalDatePickerStyle()) + + DatePicker("End Time", selection: $endTime, displayedComponents: .hourAndMinute) + .datePickerStyle(GraphicalDatePickerStyle()) + + Picker("Activity Type", selection: $selectedActivityType) { + ForEach(activityTypes, id: \.self) { activityType in + Text(activityType.activityTypeDescription).tag(activityType) + } + } + } + + Section { + Button(action: saveWorkout) { + Text("Save Workout") + } + .background( + EmptyView() + .fullScreenCover(isPresented: $isSaveSuccessful, content: { + WorkoutListView() + }) + ) + } + } + .navigationTitle("Add New Workout") + } + } + + private func saveWorkout() { + let timezoneIdentifier = TimeZone.current.identifier + let metadata = [HKMetadataKeyTimeZone: timezoneIdentifier] + let workout = HKWorkout(activityType: selectedActivityType, start: startTime, end: endTime, workoutEvents: nil, totalEnergyBurned: nil, totalDistance: nil, metadata: metadata) + + HealthKitManager.shared.save(workout) { success, error in + if success { + // Workout saved successfully + print("Workout saved successfully") + isSaveSuccessful = true + } else { + // Handle error + print("Error saving workout: \(error?.localizedDescription ?? "Unknown error")") + } + } + } +} + +struct WorkoutAddView_Previews: PreviewProvider { + static var previews: some View { + WorkoutAddView() + } +} + +// Define an extension to get the name of the activity type +extension HKWorkoutActivityType { + var activityTypeDescription: String { + switch self { + case .running: + return "Running" + case .cycling: + return "Cycling" + case .swimming: + return "Swimming" + // Add more cases as needed + default: + return "Other" + } + } +} diff --git a/WorkoutEditor/WorkoutEditView.swift b/WorkoutEditor/WorkoutEditView.swift new file mode 100644 index 0000000..5088153 --- /dev/null +++ b/WorkoutEditor/WorkoutEditView.swift @@ -0,0 +1,195 @@ +// +// WorkoutEditView.swift +// WorkoutEditor +// +// Created by Vladyslav on 23.12.2023. +// + +import SwiftUI +import HealthKit + +struct WorkoutEditView: View { + var workout: HKWorkout + + @State private var editedStartTime: Date + @State private var editedEndTime: Date + @State private var editedActivityType: HKWorkoutActivityType + + init(workout: HKWorkout) { + self.workout = workout + _editedStartTime = State(initialValue: workout.startDate) + _editedEndTime = State(initialValue: workout.endDate) + _editedActivityType = State(initialValue: workout.workoutActivityType) + } + + var activityTypes: [HKWorkoutActivityType] = [ + .running, + .cycling, + .swimming, + // Add more activity types as needed + ] + + @State private var isSaveSuccessful: Bool = false + @State private var presentingRemoveAlert = false + @State private var presentingUpdateAlert = false + + var body: some View { + NavigationView { + Form() { + Section(header: Text("Workout Details")) { + DatePicker("Start Time", selection: $editedStartTime, displayedComponents: .hourAndMinute) + .datePickerStyle(GraphicalDatePickerStyle()) + + DatePicker("End Time", selection: $editedEndTime, displayedComponents: .hourAndMinute) + .datePickerStyle(GraphicalDatePickerStyle()) + + Picker("Activity Type", selection: $editedActivityType) { + ForEach(activityTypes, id: \.self) { activityType in + Text(activityType.activityTypeDescription).tag(activityType) + } + } + } + + Section { + Button(action: saveChanges) { + Text("Save Workout") + } + .alert(isPresented: $presentingUpdateAlert) { + Alert( + title: Text("Save Changes"), + message: Text("Are you sure you want to save these changes to the workout?"), + primaryButton: .default( + Text("Save"), + action: { + // Perform the update here + confirmSaveChanges() + } + ), + secondaryButton: .cancel() + ) + } + .background( + EmptyView() + .fullScreenCover(isPresented: $isSaveSuccessful, content: { + WorkoutListView() + }) + ) + Button(action: removeWorkout) { + Text("Remove Workout") + .foregroundColor(.red) + } + .alert(isPresented: + $presentingRemoveAlert) { + Alert( + title: Text("Remove Workout"), + message: Text("Are you sure you want to remove this workout? This action cannot be undone."), + primaryButton: .destructive( + Text("Remove"), + action: { + // Perform the deletion here + confirmRemoveWorkout() + } + ), + secondaryButton: .cancel() + ) + } + .background( + EmptyView() + .fullScreenCover(isPresented: $isSaveSuccessful, content: { + WorkoutListView() + }) + ) + } + } + .navigationTitle("Workout \(formattedDate(workout.startDate))") + } + } + + private func formattedDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy" + return formatter.string(from: date) + } + + private func removeWorkout() { + presentingRemoveAlert.toggle() + } + + private func confirmRemoveWorkout() { + HealthKitManager.shared.fetchWorkout(withUUID: workout.uuid) { originalWorkout, error in + guard let originalWorkout = originalWorkout else { + return + } + + // Now, delete the original workout + HealthKitManager.shared.deleteWorkout(originalWorkout) { deleteSuccess, deleteError in + if deleteSuccess { + // Original workout deleted successfully + print("Original workout deleted successfully") + isSaveSuccessful = true + } else { + // Handle deletion error + print("Error deleting original workout: \(deleteError?.localizedDescription ?? "Unknown error")") + } + } + } + } + + private func saveChanges() { + presentingUpdateAlert.toggle() + } + + private func confirmSaveChanges() { + guard editedEndTime > editedStartTime else { + // Show an alert or some feedback to the user that the end time must be after the start time. + return + } + + // Fetch the original workout to update + HealthKitManager.shared.fetchWorkout(withUUID: workout.uuid) { originalWorkout, error in + guard let originalWorkout = originalWorkout else { + // Handle error or show an alert that the original workout couldn't be fetched + return + } + + // Update the original workout with the edited values + let updatedWorkout = HKWorkout( + activityType: editedActivityType, + start: editedStartTime, + end: editedEndTime, + workoutEvents: originalWorkout.workoutEvents, + totalEnergyBurned: originalWorkout.totalEnergyBurned, + totalDistance: originalWorkout.totalDistance, + metadata: originalWorkout.metadata + ) + + // Save the updated workout + HealthKitManager.shared.save(updatedWorkout) { success, error in + if success { + // Changes saved successfully + print("Changes saved successfully") + // Now, delete the original workout + HealthKitManager.shared.deleteWorkout(originalWorkout) { deleteSuccess, deleteError in + if deleteSuccess { + // Original workout deleted successfully + print("Original workout deleted successfully") + } else { + // Handle deletion error + print("Error deleting original workout: \(deleteError?.localizedDescription ?? "Unknown error")") + } + } + isSaveSuccessful = true + } else { + // Handle error + print("Error saving changes: \(error?.localizedDescription ?? "Unknown error")") + } + } + } + } +} + +struct WorkoutEditView_Previews: PreviewProvider { + static var previews: some View { + WorkoutEditView(workout: HKWorkout(activityType: .running, start: Date(), end: Date())) + } +} diff --git a/WorkoutEditor/WorkoutEditor.entitlements b/WorkoutEditor/WorkoutEditor.entitlements new file mode 100644 index 0000000..2ab14a2 --- /dev/null +++ b/WorkoutEditor/WorkoutEditor.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + + diff --git a/WorkoutEditor/WorkoutEditorApp.swift b/WorkoutEditor/WorkoutEditorApp.swift index a247364..1e95ba9 100644 --- a/WorkoutEditor/WorkoutEditorApp.swift +++ b/WorkoutEditor/WorkoutEditorApp.swift @@ -11,7 +11,18 @@ import SwiftUI struct WorkoutEditorApp: App { var body: some Scene { WindowGroup { - ContentView() + WorkoutListView() + .onAppear() { + HealthKitManager.shared.requestAuthorization { success, error in + if success { + // Proceed with HealthKit-related functionality + print("Authorization requested successfully") + } else { + // Handle authorization error + print("Error requesting HealthKit authorization: \(error?.localizedDescription ?? "Unknown error")") + } + } + } } } } diff --git a/WorkoutEditor/WorkoutListView.swift b/WorkoutEditor/WorkoutListView.swift new file mode 100644 index 0000000..2fe8126 --- /dev/null +++ b/WorkoutEditor/WorkoutListView.swift @@ -0,0 +1,53 @@ +// +// WorkoutListView.swift +// WorkoutEditor +// +// Created by Vladyslav on 23.12.2023. +// + +import SwiftUI +import HealthKit + +struct WorkoutListView: View { + @State private var workouts: [HKWorkout] = [] + var body: some View { + NavigationView { + List(workouts, id: \.uuid) { workout in + NavigationLink(destination: WorkoutEditView(workout: workout)) { + // Display date in "DD-MM-YYYY" format along with activity type + Text("\(formattedDate(workout.startDate)) - \(workout.workoutActivityType.activityTypeDescription)") + } + } + .onAppear { + loadWorkouts() + } + .navigationTitle("Workouts") + .navigationBarItems(trailing: NavigationLink(destination: WorkoutAddView()) { + Image(systemName: "plus") + }) + } + } + + private func loadWorkouts() { + HealthKitManager.shared.loadWorkouts { (loadedWorkouts, error) in + if let error = error { + // Handle error + print("Error loading workouts: \(error.localizedDescription)") + } else if let loadedWorkouts = loadedWorkouts { + self.workouts = loadedWorkouts + } + } + } + + private func formattedDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy" + return formatter.string(from: date) + } +} + +struct WorkoutListView_Previews: PreviewProvider { + static var previews: some View { + WorkoutListView() + } +} -- 2.51.2