Apple Fitness workout fixer + Strava uploader

workout editor implemented basically

mbenz12 fa79687e 43ec3535

+498 -31
+34 -4
WorkoutEditor.xcodeproj/project.pbxproj
··· 8 8 9 9 /* Begin PBXBuildFile section */ 10 10 06E7DFAC2B3608DD0025260F /* WorkoutEditorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */; }; 11 - 06E7DFAE2B3608DD0025260F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAD2B3608DD0025260F /* ContentView.swift */; }; 11 + 06E7DFAE2B3608DD0025260F /* WorkoutAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */; }; 12 12 06E7DFB02B3608E00025260F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 06E7DFAF2B3608E00025260F /* Assets.xcassets */; }; 13 13 06E7DFB32B3608E00025260F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 06E7DFB22B3608E00025260F /* Preview Assets.xcassets */; }; 14 + 06E7DFC32B3653F70025260F /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFC22B3653F70025260F /* HealthKitManager.swift */; }; 15 + 06E7DFC52B3654500025260F /* WorkoutListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFC42B3654500025260F /* WorkoutListView.swift */; }; 16 + 06E7DFC72B3654D50025260F /* WorkoutEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */; }; 14 17 /* End PBXBuildFile section */ 15 18 16 19 /* Begin PBXFileReference section */ 20 + 063E686B2B45161B0048778C /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; 17 21 06E7DFA82B3608DD0025260F /* WorkoutEditor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WorkoutEditor.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 22 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEditorApp.swift; sourceTree = "<group>"; }; 19 - 06E7DFAD2B3608DD0025260F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 23 + 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutAddView.swift; sourceTree = "<group>"; }; 20 24 06E7DFAF2B3608E00025260F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 21 25 06E7DFB22B3608E00025260F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 26 + 06E7DFB92B3609EA0025260F /* WorkoutEditor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WorkoutEditor.entitlements; sourceTree = "<group>"; }; 27 + 06E7DFC22B3653F70025260F /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; }; 28 + 06E7DFC42B3654500025260F /* WorkoutListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutListView.swift; sourceTree = "<group>"; }; 29 + 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutEditView.swift; sourceTree = "<group>"; }; 30 + 06E7DFC82B3668D10025260F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 22 31 /* End PBXFileReference section */ 23 32 24 33 /* Begin PBXFrameworksBuildPhase section */ ··· 32 41 /* End PBXFrameworksBuildPhase section */ 33 42 34 43 /* Begin PBXGroup section */ 44 + 063E686A2B45161A0048778C /* Frameworks */ = { 45 + isa = PBXGroup; 46 + children = ( 47 + 063E686B2B45161B0048778C /* HealthKit.framework */, 48 + ); 49 + name = Frameworks; 50 + sourceTree = "<group>"; 51 + }; 35 52 06E7DF9F2B3608DD0025260F = { 36 53 isa = PBXGroup; 37 54 children = ( 38 55 06E7DFAA2B3608DD0025260F /* WorkoutEditor */, 39 56 06E7DFA92B3608DD0025260F /* Products */, 57 + 063E686A2B45161A0048778C /* Frameworks */, 40 58 ); 41 59 sourceTree = "<group>"; 42 60 }; ··· 51 69 06E7DFAA2B3608DD0025260F /* WorkoutEditor */ = { 52 70 isa = PBXGroup; 53 71 children = ( 72 + 06E7DFC82B3668D10025260F /* Info.plist */, 73 + 06E7DFB92B3609EA0025260F /* WorkoutEditor.entitlements */, 54 74 06E7DFAB2B3608DD0025260F /* WorkoutEditorApp.swift */, 55 - 06E7DFAD2B3608DD0025260F /* ContentView.swift */, 75 + 06E7DFAD2B3608DD0025260F /* WorkoutAddView.swift */, 76 + 06E7DFC62B3654D50025260F /* WorkoutEditView.swift */, 77 + 06E7DFC42B3654500025260F /* WorkoutListView.swift */, 78 + 06E7DFC22B3653F70025260F /* HealthKitManager.swift */, 56 79 06E7DFAF2B3608E00025260F /* Assets.xcassets */, 57 80 06E7DFB12B3608E00025260F /* Preview Content */, 58 81 ); ··· 137 160 isa = PBXSourcesBuildPhase; 138 161 buildActionMask = 2147483647; 139 162 files = ( 140 - 06E7DFAE2B3608DD0025260F /* ContentView.swift in Sources */, 163 + 06E7DFAE2B3608DD0025260F /* WorkoutAddView.swift in Sources */, 141 164 06E7DFAC2B3608DD0025260F /* WorkoutEditorApp.swift in Sources */, 165 + 06E7DFC52B3654500025260F /* WorkoutListView.swift in Sources */, 166 + 06E7DFC32B3653F70025260F /* HealthKitManager.swift in Sources */, 167 + 06E7DFC72B3654D50025260F /* WorkoutEditView.swift in Sources */, 142 168 ); 143 169 runOnlyForDeploymentPostprocessing = 0; 144 170 }; ··· 264 290 buildSettings = { 265 291 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 266 292 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 293 + CODE_SIGN_ENTITLEMENTS = WorkoutEditor/WorkoutEditor.entitlements; 267 294 CODE_SIGN_STYLE = Automatic; 268 295 CURRENT_PROJECT_VERSION = 1; 269 296 DEVELOPMENT_ASSET_PATHS = "\"WorkoutEditor/Preview Content\""; 270 297 ENABLE_PREVIEWS = YES; 271 298 GENERATE_INFOPLIST_FILE = YES; 299 + INFOPLIST_FILE = WorkoutEditor/Info.plist; 272 300 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 273 301 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 274 302 INFOPLIST_KEY_UILaunchScreen_Generation = YES; ··· 292 320 buildSettings = { 293 321 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 294 322 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 323 + CODE_SIGN_ENTITLEMENTS = WorkoutEditor/WorkoutEditor.entitlements; 295 324 CODE_SIGN_STYLE = Automatic; 296 325 CURRENT_PROJECT_VERSION = 1; 297 326 DEVELOPMENT_ASSET_PATHS = "\"WorkoutEditor/Preview Content\""; 298 327 ENABLE_PREVIEWS = YES; 299 328 GENERATE_INFOPLIST_FILE = YES; 329 + INFOPLIST_FILE = WorkoutEditor/Info.plist; 300 330 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 301 331 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 302 332 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
-26
WorkoutEditor/ContentView.swift
··· 1 - // 2 - // ContentView.swift 3 - // WorkoutEditor 4 - // 5 - // Created by Vladyslav on 22.12.2023. 6 - // 7 - 8 - import SwiftUI 9 - 10 - struct ContentView: View { 11 - var body: some View { 12 - VStack { 13 - Image(systemName: "globe") 14 - .imageScale(.large) 15 - .foregroundColor(.accentColor) 16 - Text("Hello, world!") 17 - } 18 - .padding() 19 - } 20 - } 21 - 22 - struct ContentView_Previews: PreviewProvider { 23 - static var previews: some View { 24 - ContentView() 25 - } 26 - }
+87
WorkoutEditor/HealthKitManager.swift
··· 1 + // 2 + // HealthKitManager.swift 3 + // WorkoutEditor 4 + // 5 + // Created by Vladyslav on 23.12.2023. 6 + // 7 + 8 + import Foundation 9 + import HealthKit 10 + 11 + class HealthKitManager { 12 + static let shared = HealthKitManager() 13 + 14 + private let healthStore = HKHealthStore() 15 + 16 + // Request authorization for specific health data types 17 + func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) { 18 + guard HKHealthStore.isHealthDataAvailable() else { 19 + completion(false, nil) 20 + return 21 + } 22 + 23 + let typesToRead: Set<HKObjectType> = [HKObjectType.workoutType()] 24 + 25 + let typesToWrite: Set<HKSampleType> = [HKObjectType.workoutType()] 26 + 27 + healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) { (success, error) in 28 + completion(success, error) 29 + } 30 + } 31 + 32 + func save(_ workout: HKWorkout, completion: @escaping (Bool, Error?) -> Void) { 33 + self.healthStore.save(workout) { success, error in 34 + completion(success, error) 35 + } 36 + } 37 + 38 + func loadWorkouts(completion: @escaping ([HKWorkout]?, Error?) -> Void) { 39 + let workoutType = HKObjectType.workoutType() 40 + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) 41 + let query = HKSampleQuery(sampleType: workoutType, predicate: nil, limit: 100, sortDescriptors: [sortDescriptor]) { (query, samples, error) in 42 + guard let workouts = samples as? [HKWorkout] else { 43 + completion(nil, error) 44 + return 45 + } 46 + completion(workouts, error) 47 + } 48 + 49 + self.healthStore.execute(query) 50 + } 51 + 52 + func fetchWorkout(withUUID uuid: UUID, completion: @escaping (HKWorkout?, Error?) -> Void) { 53 + guard HKHealthStore.isHealthDataAvailable() else { 54 + completion(nil, HealthKitError.healthDataNotAvailable) 55 + return 56 + } 57 + 58 + let workoutType = HKObjectType.workoutType() 59 + let predicate = HKQuery.predicateForObject(with: uuid) 60 + 61 + let query = HKSampleQuery( 62 + sampleType: workoutType, 63 + predicate: predicate, 64 + limit: HKObjectQueryNoLimit, 65 + sortDescriptors: nil 66 + ) { (query, samples, error) in 67 + guard let workouts = samples as? [HKWorkout], let workout = workouts.first else { 68 + completion(nil, error) 69 + return 70 + } 71 + completion(workout, nil) 72 + } 73 + 74 + healthStore.execute(query) 75 + } 76 + 77 + func deleteWorkout(_ workout: HKWorkout, completion: @escaping (Bool, Error?) -> Void) { 78 + healthStore.delete(workout) { success, error in 79 + completion(success, error) 80 + } 81 + } 82 + } 83 + 84 + enum HealthKitError: Error { 85 + case healthDataNotAvailable 86 + // Add more error cases as needed 87 + }
+12
WorkoutEditor/Info.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>UIBackgroundModes</key> 6 + <array/> 7 + <key>NSHealthShareUsageDescription</key> 8 + <string>We need to read data</string> 9 + <key>NSHealthUpdateUsageDescription</key> 10 + <string>We need to write data</string> 11 + </dict> 12 + </plist>
+95
WorkoutEditor/WorkoutAddView.swift
··· 1 + // 2 + // ContentView.swift 3 + // WorkoutEditor 4 + // 5 + // Created by Vladyslav on 22.12.2023. 6 + // 7 + 8 + import SwiftUI 9 + import HealthKit 10 + 11 + struct WorkoutAddView: View { 12 + @State private var selectedActivityType: HKWorkoutActivityType = .running 13 + @State private var startTime = Date() 14 + @State private var endTime = Date() 15 + 16 + var activityTypes: [HKWorkoutActivityType] = [ 17 + .running, 18 + .cycling, 19 + .swimming, 20 + // Add more activity types as needed 21 + ] 22 + @State private var isSaveSuccessful: Bool = false 23 + var body: some View { 24 + NavigationView { 25 + Form() { 26 + Section(header: Text("New Workout Details")) { 27 + DatePicker("Start Time", selection: $startTime, displayedComponents: .hourAndMinute) 28 + .datePickerStyle(GraphicalDatePickerStyle()) 29 + 30 + DatePicker("End Time", selection: $endTime, displayedComponents: .hourAndMinute) 31 + .datePickerStyle(GraphicalDatePickerStyle()) 32 + 33 + Picker("Activity Type", selection: $selectedActivityType) { 34 + ForEach(activityTypes, id: \.self) { activityType in 35 + Text(activityType.activityTypeDescription).tag(activityType) 36 + } 37 + } 38 + } 39 + 40 + Section { 41 + Button(action: saveWorkout) { 42 + Text("Save Workout") 43 + } 44 + .background( 45 + EmptyView() 46 + .fullScreenCover(isPresented: $isSaveSuccessful, content: { 47 + WorkoutListView() 48 + }) 49 + ) 50 + } 51 + } 52 + .navigationTitle("Add New Workout") 53 + } 54 + } 55 + 56 + private func saveWorkout() { 57 + let timezoneIdentifier = TimeZone.current.identifier 58 + let metadata = [HKMetadataKeyTimeZone: timezoneIdentifier] 59 + let workout = HKWorkout(activityType: selectedActivityType, start: startTime, end: endTime, workoutEvents: nil, totalEnergyBurned: nil, totalDistance: nil, metadata: metadata) 60 + 61 + HealthKitManager.shared.save(workout) { success, error in 62 + if success { 63 + // Workout saved successfully 64 + print("Workout saved successfully") 65 + isSaveSuccessful = true 66 + } else { 67 + // Handle error 68 + print("Error saving workout: \(error?.localizedDescription ?? "Unknown error")") 69 + } 70 + } 71 + } 72 + } 73 + 74 + struct WorkoutAddView_Previews: PreviewProvider { 75 + static var previews: some View { 76 + WorkoutAddView() 77 + } 78 + } 79 + 80 + // Define an extension to get the name of the activity type 81 + extension HKWorkoutActivityType { 82 + var activityTypeDescription: String { 83 + switch self { 84 + case .running: 85 + return "Running" 86 + case .cycling: 87 + return "Cycling" 88 + case .swimming: 89 + return "Swimming" 90 + // Add more cases as needed 91 + default: 92 + return "Other" 93 + } 94 + } 95 + }
+195
WorkoutEditor/WorkoutEditView.swift
··· 1 + // 2 + // WorkoutEditView.swift 3 + // WorkoutEditor 4 + // 5 + // Created by Vladyslav on 23.12.2023. 6 + // 7 + 8 + import SwiftUI 9 + import HealthKit 10 + 11 + struct WorkoutEditView: View { 12 + var workout: HKWorkout 13 + 14 + @State private var editedStartTime: Date 15 + @State private var editedEndTime: Date 16 + @State private var editedActivityType: HKWorkoutActivityType 17 + 18 + init(workout: HKWorkout) { 19 + self.workout = workout 20 + _editedStartTime = State(initialValue: workout.startDate) 21 + _editedEndTime = State(initialValue: workout.endDate) 22 + _editedActivityType = State(initialValue: workout.workoutActivityType) 23 + } 24 + 25 + var activityTypes: [HKWorkoutActivityType] = [ 26 + .running, 27 + .cycling, 28 + .swimming, 29 + // Add more activity types as needed 30 + ] 31 + 32 + @State private var isSaveSuccessful: Bool = false 33 + @State private var presentingRemoveAlert = false 34 + @State private var presentingUpdateAlert = false 35 + 36 + var body: some View { 37 + NavigationView { 38 + Form() { 39 + Section(header: Text("Workout Details")) { 40 + DatePicker("Start Time", selection: $editedStartTime, displayedComponents: .hourAndMinute) 41 + .datePickerStyle(GraphicalDatePickerStyle()) 42 + 43 + DatePicker("End Time", selection: $editedEndTime, displayedComponents: .hourAndMinute) 44 + .datePickerStyle(GraphicalDatePickerStyle()) 45 + 46 + Picker("Activity Type", selection: $editedActivityType) { 47 + ForEach(activityTypes, id: \.self) { activityType in 48 + Text(activityType.activityTypeDescription).tag(activityType) 49 + } 50 + } 51 + } 52 + 53 + Section { 54 + Button(action: saveChanges) { 55 + Text("Save Workout") 56 + } 57 + .alert(isPresented: $presentingUpdateAlert) { 58 + Alert( 59 + title: Text("Save Changes"), 60 + message: Text("Are you sure you want to save these changes to the workout?"), 61 + primaryButton: .default( 62 + Text("Save"), 63 + action: { 64 + // Perform the update here 65 + confirmSaveChanges() 66 + } 67 + ), 68 + secondaryButton: .cancel() 69 + ) 70 + } 71 + .background( 72 + EmptyView() 73 + .fullScreenCover(isPresented: $isSaveSuccessful, content: { 74 + WorkoutListView() 75 + }) 76 + ) 77 + Button(action: removeWorkout) { 78 + Text("Remove Workout") 79 + .foregroundColor(.red) 80 + } 81 + .alert(isPresented: 82 + $presentingRemoveAlert) { 83 + Alert( 84 + title: Text("Remove Workout"), 85 + message: Text("Are you sure you want to remove this workout? This action cannot be undone."), 86 + primaryButton: .destructive( 87 + Text("Remove"), 88 + action: { 89 + // Perform the deletion here 90 + confirmRemoveWorkout() 91 + } 92 + ), 93 + secondaryButton: .cancel() 94 + ) 95 + } 96 + .background( 97 + EmptyView() 98 + .fullScreenCover(isPresented: $isSaveSuccessful, content: { 99 + WorkoutListView() 100 + }) 101 + ) 102 + } 103 + } 104 + .navigationTitle("Workout \(formattedDate(workout.startDate))") 105 + } 106 + } 107 + 108 + private func formattedDate(_ date: Date) -> String { 109 + let formatter = DateFormatter() 110 + formatter.dateFormat = "dd-MM-yyyy" 111 + return formatter.string(from: date) 112 + } 113 + 114 + private func removeWorkout() { 115 + presentingRemoveAlert.toggle() 116 + } 117 + 118 + private func confirmRemoveWorkout() { 119 + HealthKitManager.shared.fetchWorkout(withUUID: workout.uuid) { originalWorkout, error in 120 + guard let originalWorkout = originalWorkout else { 121 + return 122 + } 123 + 124 + // Now, delete the original workout 125 + HealthKitManager.shared.deleteWorkout(originalWorkout) { deleteSuccess, deleteError in 126 + if deleteSuccess { 127 + // Original workout deleted successfully 128 + print("Original workout deleted successfully") 129 + isSaveSuccessful = true 130 + } else { 131 + // Handle deletion error 132 + print("Error deleting original workout: \(deleteError?.localizedDescription ?? "Unknown error")") 133 + } 134 + } 135 + } 136 + } 137 + 138 + private func saveChanges() { 139 + presentingUpdateAlert.toggle() 140 + } 141 + 142 + private func confirmSaveChanges() { 143 + guard editedEndTime > editedStartTime else { 144 + // Show an alert or some feedback to the user that the end time must be after the start time. 145 + return 146 + } 147 + 148 + // Fetch the original workout to update 149 + HealthKitManager.shared.fetchWorkout(withUUID: workout.uuid) { originalWorkout, error in 150 + guard let originalWorkout = originalWorkout else { 151 + // Handle error or show an alert that the original workout couldn't be fetched 152 + return 153 + } 154 + 155 + // Update the original workout with the edited values 156 + let updatedWorkout = HKWorkout( 157 + activityType: editedActivityType, 158 + start: editedStartTime, 159 + end: editedEndTime, 160 + workoutEvents: originalWorkout.workoutEvents, 161 + totalEnergyBurned: originalWorkout.totalEnergyBurned, 162 + totalDistance: originalWorkout.totalDistance, 163 + metadata: originalWorkout.metadata 164 + ) 165 + 166 + // Save the updated workout 167 + HealthKitManager.shared.save(updatedWorkout) { success, error in 168 + if success { 169 + // Changes saved successfully 170 + print("Changes saved successfully") 171 + // Now, delete the original workout 172 + HealthKitManager.shared.deleteWorkout(originalWorkout) { deleteSuccess, deleteError in 173 + if deleteSuccess { 174 + // Original workout deleted successfully 175 + print("Original workout deleted successfully") 176 + } else { 177 + // Handle deletion error 178 + print("Error deleting original workout: \(deleteError?.localizedDescription ?? "Unknown error")") 179 + } 180 + } 181 + isSaveSuccessful = true 182 + } else { 183 + // Handle error 184 + print("Error saving changes: \(error?.localizedDescription ?? "Unknown error")") 185 + } 186 + } 187 + } 188 + } 189 + } 190 + 191 + struct WorkoutEditView_Previews: PreviewProvider { 192 + static var previews: some View { 193 + WorkoutEditView(workout: HKWorkout(activityType: .running, start: Date(), end: Date())) 194 + } 195 + }
+10
WorkoutEditor/WorkoutEditor.entitlements
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>com.apple.developer.healthkit</key> 6 + <true/> 7 + <key>com.apple.developer.healthkit.access</key> 8 + <array/> 9 + </dict> 10 + </plist>
+12 -1
WorkoutEditor/WorkoutEditorApp.swift
··· 11 11 struct WorkoutEditorApp: App { 12 12 var body: some Scene { 13 13 WindowGroup { 14 - ContentView() 14 + WorkoutListView() 15 + .onAppear() { 16 + HealthKitManager.shared.requestAuthorization { success, error in 17 + if success { 18 + // Proceed with HealthKit-related functionality 19 + print("Authorization requested successfully") 20 + } else { 21 + // Handle authorization error 22 + print("Error requesting HealthKit authorization: \(error?.localizedDescription ?? "Unknown error")") 23 + } 24 + } 25 + } 15 26 } 16 27 } 17 28 }
+53
WorkoutEditor/WorkoutListView.swift
··· 1 + // 2 + // WorkoutListView.swift 3 + // WorkoutEditor 4 + // 5 + // Created by Vladyslav on 23.12.2023. 6 + // 7 + 8 + import SwiftUI 9 + import HealthKit 10 + 11 + struct WorkoutListView: View { 12 + @State private var workouts: [HKWorkout] = [] 13 + var body: some View { 14 + NavigationView { 15 + List(workouts, id: \.uuid) { workout in 16 + NavigationLink(destination: WorkoutEditView(workout: workout)) { 17 + // Display date in "DD-MM-YYYY" format along with activity type 18 + Text("\(formattedDate(workout.startDate)) - \(workout.workoutActivityType.activityTypeDescription)") 19 + } 20 + } 21 + .onAppear { 22 + loadWorkouts() 23 + } 24 + .navigationTitle("Workouts") 25 + .navigationBarItems(trailing: NavigationLink(destination: WorkoutAddView()) { 26 + Image(systemName: "plus") 27 + }) 28 + } 29 + } 30 + 31 + private func loadWorkouts() { 32 + HealthKitManager.shared.loadWorkouts { (loadedWorkouts, error) in 33 + if let error = error { 34 + // Handle error 35 + print("Error loading workouts: \(error.localizedDescription)") 36 + } else if let loadedWorkouts = loadedWorkouts { 37 + self.workouts = loadedWorkouts 38 + } 39 + } 40 + } 41 + 42 + private func formattedDate(_ date: Date) -> String { 43 + let formatter = DateFormatter() 44 + formatter.dateFormat = "dd-MM-yyyy" 45 + return formatter.string(from: date) 46 + } 47 + } 48 + 49 + struct WorkoutListView_Previews: PreviewProvider { 50 + static var previews: some View { 51 + WorkoutListView() 52 + } 53 + }