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