From 0f0e68dcf323edbebb0f495ac892e106a3f1d770 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:09:32 -0300 Subject: [PATCH 01/14] Add workout activities to sample --- .../HealthKitCore/Workout/Quantity.swift | 37 ++++++++ .../Workout/WorkoutActivity.swift | 69 +++++++++++++++ .../Workout/WorkoutConfiguration.swift | 64 ++++++++++++++ .../Workout/WorkoutQueries.swift | 84 +++++++++++++++++++ .../HealthKitCore/Workout/WorkoutSample.swift | 10 ++- 5 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift create mode 100644 v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift create mode 100644 v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift new file mode 100644 index 00000000..c585fbdd --- /dev/null +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift @@ -0,0 +1,37 @@ +import Foundation +import HealthKit + +public struct WorkoutQuantity: Codable { + public let unit: HKUnit + public let doubleValue: Double + + private enum CodingKeys: String, CodingKey { + case unit + case doubleValue + } + + public init(unit: HKUnit, doubleValue: Double) { + self.unit = unit + self.doubleValue = doubleValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let unitString = try container.decode(String.self, forKey: .unit) + let doubleValue = try container.decode(Double.self, forKey: .doubleValue) + self.unit = HKUnit(from: unitString) + self.doubleValue = doubleValue + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(unit.unitString, forKey: .unit) + try container.encode(doubleValue, forKey: .doubleValue) + } +} + +extension HKQuantity { + public convenience init(quantity: WorkoutQuantity) { + self.init(unit: quantity.unit, doubleValue: quantity.doubleValue) + } +} diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift new file mode 100644 index 00000000..43f0369c --- /dev/null +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift @@ -0,0 +1,69 @@ +import Foundation +import HealthKit + +public struct WorkoutActivity: Codable { + public let workoutConfiguration: WorkoutConfiguration + public let startDate: String + public let endDate: String? + public let metadata: [String: Any]? + + private enum CodingKeys: String, CodingKey { + case workoutConfiguration + case startDate + case endDate + case metadata + } + + public init( + workoutConfiguration: WorkoutConfiguration, + startDate: String, + endDate: String?, + metadata: [String: Any]? = nil + ) { + self.workoutConfiguration = workoutConfiguration + self.startDate = startDate + self.endDate = endDate + self.metadata = metadata + } + + @available(iOS 16.0, *) + public init(activity: HKWorkoutActivity) { + let configuration = activity.workoutConfiguration + self.init( + workoutConfiguration: .init( + workoutActivityType: configuration.activityType, + workoutLocationType: configuration.locationType, + workoutSwimmingLocationType: configuration.swimmingLocationType + ), + startDate: activity.startDate.toIsoString()!, + endDate: activity.endDate?.toIsoString(), + metadata: activity.metadata + ) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(workoutConfiguration, forKey: .workoutConfiguration) + try container.encode(startDate, forKey: .startDate) + try container.encodeIfPresent(endDate, forKey: .endDate) + + if let metadata = metadata { + let jsonData = try JSONSerialization.data(withJSONObject: metadata, options: []) + try container.encode(jsonData, forKey: .metadata) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + workoutConfiguration = try container.decode(WorkoutConfiguration.self, forKey: .workoutConfiguration) + startDate = try container.decode(String.self, forKey: .startDate) + endDate = try container.decodeIfPresent(String.self, forKey: .endDate) + + if let metadataData = try container.decodeIfPresent(Data.self, forKey: .metadata) { + metadata = try JSONSerialization.jsonObject(with: metadataData, options: []) as? [String: Any] + } else { + metadata = nil + } + } +} diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift new file mode 100644 index 00000000..01c5a79d --- /dev/null +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift @@ -0,0 +1,64 @@ +import Foundation +import HealthKit + +public struct WorkoutConfiguration: Codable { + public let workoutActivityType: HKWorkoutActivityType? + public let workoutLocationType: HKWorkoutSessionLocationType? + public let workoutSwimmingLocationType: HKWorkoutSwimmingLocationType? + public let workoutLapLength: WorkoutQuantity? + + private enum CodingKeys: String, CodingKey { + case workoutActivityType + case workoutLocationType + case workoutSwimmingLocationType + case workoutLapLength + } + + public init( + workoutActivityType: HKWorkoutActivityType? = .other, + workoutLocationType: HKWorkoutSessionLocationType = .unknown, + workoutSwimmingLocationType: HKWorkoutSwimmingLocationType? = nil, + workoutLapLength: WorkoutQuantity? = nil + ) { + self.workoutActivityType = workoutActivityType + self.workoutLocationType = workoutLocationType + self.workoutSwimmingLocationType = workoutSwimmingLocationType + self.workoutLapLength = workoutLapLength + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(workoutActivityType?.rawValue, forKey: .workoutActivityType) + try container.encodeIfPresent(workoutLocationType?.rawValue, forKey: .workoutLocationType) + try container.encodeIfPresent(workoutSwimmingLocationType?.rawValue, forKey: .workoutSwimmingLocationType) + try container.encodeIfPresent(workoutLapLength, forKey: .workoutLapLength) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let workoutActivityTypeRawValue = try container.decodeIfPresent(Int.self, forKey: .workoutActivityType) + let locationTypeRawValue = try container.decodeIfPresent(Int.self, forKey: .workoutLocationType) + let swimmingLocationTypeRawValue = try container.decodeIfPresent(Int.self, forKey: .workoutSwimmingLocationType) + let lapLength = try container.decodeIfPresent(WorkoutQuantity.self, forKey: .workoutLapLength) + + if let rawActivityRawValue = workoutActivityTypeRawValue { + self.workoutActivityType = HKWorkoutActivityType(rawValue: UInt(rawActivityRawValue)) + } else { + self.workoutActivityType = nil + } + + if let rawLocationTypeValue = locationTypeRawValue { + self.workoutLocationType = HKWorkoutSessionLocationType(rawValue: rawLocationTypeValue) + } else { + self.workoutLocationType = .unknown + } + + if let rawSwimmingLocationTypeValue = swimmingLocationTypeRawValue { + self.workoutSwimmingLocationType = HKWorkoutSwimmingLocationType(rawValue: rawSwimmingLocationTypeValue) + } else { + self.workoutSwimmingLocationType = nil + } + + self.workoutLapLength = lapLength + } +} diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueries.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueries.swift index f058fb2d..5efd5d49 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueries.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueries.swift @@ -44,6 +44,88 @@ public extension HealthKitCore { totalEnergyBurned: Double?, totalDistance: Double?, metadata: [String: Any]? + ) async throws -> HKWorkout? { + try await handleSaveCompletedWorkout( + activityType: activityType, + startDate: startDate, + endDate: endDate, + totalEnergyBurned: totalEnergyBurned, + totalDistance: totalDistance, + metadata: metadata, + completion: nil + ) + } + + /// Saves a completed workout to HealthKit with specified parameters and additional activities. + /// - Parameters: + /// - activityType: The type of physical activity for the workout. + /// - startDate: The start date and time of the workout. + /// - endDate: The end date and time of the workout. + /// - totalEnergyBurned: The total energy burned during the workout (in kilocalories). + /// - totalDistance: The total distance covered during the workout (in kilometers). + /// - activities: Optional array of `WorkoutActivity` to include in the workout. + /// - metadata: Optional metadata for the workout. + /// - Throws: An error if the workout data cannot be saved to HealthKit. + @available(iOS 16.0, *) + func saveCompletedWorkout( + activityType: HKWorkoutActivityType, + startDate: Date, + endDate: Date, + totalEnergyBurned: Double?, + totalDistance: Double?, + activities: [WorkoutActivity]?, + metadata: [String: Any]? + ) async throws -> HKWorkout? { + let handleActivities: ((HKWorkoutConfiguration, HKWorkoutBuilder) async throws -> Void)? = activities != nil ? { configuration, builder in + if let activities { + for activity in activities { + let workoutConfiguration = activity.workoutConfiguration + if let activityType = workoutConfiguration.workoutActivityType { + configuration.activityType = activityType + } + + if let locationType = workoutConfiguration.workoutLocationType { + configuration.locationType = locationType + } + + if let swimmingLocationType = workoutConfiguration.workoutSwimmingLocationType { + configuration.swimmingLocationType = swimmingLocationType + } + + if let lapLength = workoutConfiguration.workoutLapLength { + configuration.lapLength = HKQuantity(quantity: lapLength) + } + try await builder.addWorkoutActivity( + HKWorkoutActivity( + workoutConfiguration: configuration, + start: activity.startDate.fromIsoStringToDate(), + end: activity.endDate?.fromIsoStringToDate(), + metadata: activity.metadata + ) + ) + } + } + } : nil + + return try await handleSaveCompletedWorkout( + activityType: activityType, + startDate: startDate, + endDate: endDate, + totalEnergyBurned: totalEnergyBurned, + totalDistance: totalDistance, + metadata: metadata, + completion: handleActivities + ) + } + + private func handleSaveCompletedWorkout( + activityType: HKWorkoutActivityType, + startDate: Date, + endDate: Date, + totalEnergyBurned: Double?, + totalDistance: Double?, + metadata: [String: Any]?, + completion: ((HKWorkoutConfiguration, HKWorkoutBuilder) async throws -> Void)? ) async throws -> HKWorkout? { let configuration = HKWorkoutConfiguration() configuration.activityType = activityType @@ -74,6 +156,8 @@ public extension HealthKitCore { samples.append(distanceSample) } + try await completion?(configuration, workoutBuilder) + if let metadata { try await workoutBuilder.addMetadata(metadata) } diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift index 3399d709..4edc4cae 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift @@ -18,10 +18,12 @@ public struct WorkoutSample: Encodable { /// The activity type of the workout as a raw unsigned integer value. public let activityType: UInt + // Representing additional activities that are part of the workout. + public let workoutActivities: [WorkoutActivity]? + /// Additional metadata associated with the workout sample. public let metadata: [String: String]? - // TODO: Add activities | var workoutActivities: [HKWorkoutActivity] // TODO: Add workout events | var workoutEvents: [HKWorkoutEvent]? // TODO: Add statistics public var statistics: [QuantityType : QuantitySample] @@ -34,6 +36,12 @@ public struct WorkoutSample: Encodable { self.endDate = workout.endDate.toIsoString() ?? "" self.duration = workout.duration self.activityType = workout.workoutActivityType.rawValue + + if #available(iOS 16.0, *) { + workoutActivities = workout.workoutActivities.compactMap({ WorkoutActivity(activity: $0) }) + } else { + workoutActivities = nil + } self.metadata = workout.metadata?.mapValues(String.init(describing:)) ?? [:] } } From ff7f08fb1782c1a9ba0512583415e0410e77170f Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:10:20 -0300 Subject: [PATCH 02/14] Update example app with activities --- .../RNHealthKitCoreApp/ContentView.swift | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift b/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift index 48a8daa5..e5a30cb8 100644 --- a/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift +++ b/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift @@ -30,23 +30,52 @@ struct ContentView: View { } } - func saveWorkoutWithMetadata(core: HealthKitCore) async { - let unit = HKUnit(from: "min") - let rawMetadata: [String: Any] = [ - "HKIndoorWorkout": true, - "HKFitnessMachineDuration": ["unit": "min", "doubleValue": 30.0], - ] - - let processedMetadata = try! WorkoutHelper.processWorkoutMetadata(rawMetadata) - - dump(try! await core.saveCompletedWorkout( - activityType: .coreTraining, - startDate: Calendar(identifier: .gregorian).date(byAdding: .minute, value: -2, to: .now)!, - endDate: Date(), - totalEnergyBurned: 120, - totalDistance: 2000, - metadata: processedMetadata - )) + private func saveWorkoutWithMetadata(core: HealthKitCore) async throws { + let startDate = Calendar(identifier: .gregorian).date(byAdding: .minute, value: -15, to: .now)! + let activityStartTime = Calendar(identifier: .gregorian).date(byAdding: .minute, value: -14, to: .now)! + + let conf1 = WorkoutConfiguration( + workoutActivityType: .swimming, + workoutLocationType: .outdoor, + workoutSwimmingLocationType: .openWater + ) + let conf2 = WorkoutConfiguration( + workoutActivityType: .running, + workoutLocationType: .outdoor, + workoutSwimmingLocationType: .unknown + ) + + let activity1 = WorkoutActivity( + workoutConfiguration: conf1, + startDate: activityStartTime.toIsoString()!, + endDate: Calendar(identifier: .gregorian).date(byAdding: .minute, value: -10, to: .now)!.toIsoString(), + metadata: nil + ) + let activity2 = WorkoutActivity( + workoutConfiguration: conf2, + startDate: Calendar(identifier: .gregorian).date(byAdding: .minute, value: -5, to: .now)!.toIsoString()!, + endDate: Calendar(identifier: .gregorian).date(byAdding: .minute, value: -1, to: .now)!.toIsoString(), + metadata: nil + ) + + if #available(iOS 16.0, *) { + print(try await core.saveCompletedWorkout( + activityType: .swimBikeRun, + startDate: startDate, endDate: Date(), + totalEnergyBurned: 120, + totalDistance: 2000, + activities: [activity1, activity2], + metadata: nil + )) + } else { + print(try await core.saveCompletedWorkout( + activityType: .running, + startDate: startDate, endDate: Date(), + totalEnergyBurned: 120, + totalDistance: 2000, + metadata: nil + )) + } } } @@ -55,3 +84,11 @@ struct ContentView_Previews: PreviewProvider { ContentView() } } + +private extension Date { + func toIsoString() -> String? { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" + return formatter.string(from: self) + } +} From 3b8d8ee3f01f6d1bd4fc32bb529cb1a33fb32657 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:10:42 -0300 Subject: [PATCH 03/14] Update saveWorkout to include workout activities --- .../HealthKitCore.xcodeproj/project.pbxproj | 12 +++++++ .../RNHealthKitWrapper.swift | 35 ++++++++++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj b/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj index 4747819e..0e26297a 100644 --- a/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj +++ b/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ 7E9A84F52A544A1A004622E5 /* QuantityQueriesParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E9A84F42A544A1A004622E5 /* QuantityQueriesParameters.swift */; }; 7E9A84F72A548A39004622E5 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E9A84F62A548A39004622E5 /* Utils.swift */; }; D31BF2482B10FC7C00005EE7 /* WorkoutHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D31BF2472B10FC7C00005EE7 /* WorkoutHelper.swift */; }; + D3785B392B16494A00B8C30A /* WorkoutActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B382B16494A00B8C30A /* WorkoutActivity.swift */; }; + D3785B3D2B16558800B8C30A /* Quantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B3C2B16558800B8C30A /* Quantity.swift */; }; + D3785B412B17996E00B8C30A /* WorkoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B402B17996E00B8C30A /* WorkoutConfiguration.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +51,9 @@ 7EB70C892A61ABAF003EE217 /* RNHealthKitWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNHealthKitWrapper.m; sourceTree = ""; }; 7EE4AA502A669CA200CC9EEF /* RNHealthKitWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNHealthKitWrapper.swift; sourceTree = ""; }; D31BF2472B10FC7C00005EE7 /* WorkoutHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutHelper.swift; sourceTree = ""; }; + D3785B382B16494A00B8C30A /* WorkoutActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutActivity.swift; sourceTree = ""; }; + D3785B3C2B16558800B8C30A /* Quantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quantity.swift; sourceTree = ""; }; + D3785B402B17996E00B8C30A /* WorkoutConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkoutConfiguration.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -74,10 +80,13 @@ 7E4BFFE42A9FC0D500FB1383 /* Workout */ = { isa = PBXGroup; children = ( + D3785B402B17996E00B8C30A /* WorkoutConfiguration.swift */, 7E4BFFE92AAA423100FB1383 /* WorkoutSample.swift */, 7E4BFFE52A9FC0ED00FB1383 /* WorkoutQueries.swift */, 7E4BFFE72AAA421200FB1383 /* WorkoutQueryParameters.swift */, D31BF2472B10FC7C00005EE7 /* WorkoutHelper.swift */, + D3785B382B16494A00B8C30A /* WorkoutActivity.swift */, + D3785B3C2B16558800B8C30A /* Quantity.swift */, ); path = Workout; sourceTree = ""; @@ -183,7 +192,10 @@ 7E9A84F52A544A1A004622E5 /* QuantityQueriesParameters.swift in Sources */, 7E9A84F32A5449C2004622E5 /* HealthKitTypes.swift in Sources */, 7E9A84CC2A5312D7004622E5 /* HealthKitCore.swift in Sources */, + D3785B3D2B16558800B8C30A /* Quantity.swift in Sources */, D31BF2482B10FC7C00005EE7 /* WorkoutHelper.swift in Sources */, + D3785B392B16494A00B8C30A /* WorkoutActivity.swift in Sources */, + D3785B412B17996E00B8C30A /* WorkoutConfiguration.swift in Sources */, 7E5AEBB32A5EAF2800F74829 /* QuantityQueries.swift in Sources */, 7E4BFFEE2AAA6D6D00FB1383 /* QueryParameters.swift in Sources */, 7E4BFFEA2AAA423100FB1383 /* WorkoutSample.swift in Sources */, diff --git a/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift b/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift index 9822160a..f888fefb 100644 --- a/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift +++ b/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift @@ -224,14 +224,33 @@ class RNHealthKitWrapper: NSObject { processedMetadata = try WorkoutHelper.processWorkoutMetadata(metadata) } - try await core?.saveCompletedWorkout( - activityType: activityType, - startDate: startDate, - endDate: endDate, - totalEnergyBurned: workout["totalEnergyBurned"] as? Double, - totalDistance: workout["totalDistance"] as? Double, - metadata: processedMetadata - ) + let workoutActivities: [WorkoutActivity] + if let activities = workout["activities"] { + let activityData = Data(activities) + workoutActivities = try JSONDecoder().decode([WorkoutActivity].self, from: activityData) + } + + if #available(iOS 16.0, *) { + try await core?.saveCompletedWorkout( + activityType: activityType, + startDate: startDate, + endDate: endDate, + totalEnergyBurned: workout["totalEnergyBurned"] as? Double, + totalDistance: workout["totalDistance"] as? Double, + activities: workoutActivities, + metadata: processedMetadata + ) + } else { + try await core?.saveCompletedWorkout( + activityType: activityType, + startDate: startDate, + endDate: endDate, + totalEnergyBurned: workout["totalEnergyBurned"] as? Double, + totalDistance: workout["totalDistance"] as? Double, + metadata: processedMetadata + ) + } + resolve(true) } catch { reject("saveWorkout", error.localizedDescription, error) From 7a4a2216d567f0d1dd1348cdc182fd84fed17d73 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:11:22 -0300 Subject: [PATCH 04/14] Fix issue with workout activitiy type assigned numbers --- example/App.tsx | 38 ++++++++++++++++++++++++++++++++++++-- index.ts | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index 656963be..00feaa0c 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -16,6 +16,8 @@ import RNHealthKit, { Interval, WorkoutActivityType, WorkoutMetadataKey, + WorkoutSessionLocationType, + WorkoutSwimmingLocationType, } from 'react-native-health'; RNHealthKit.initHealthKit( @@ -63,16 +65,48 @@ async function runWorkoutQuery() { const result = await RNHealthKit.getWorkouts({ startDate: new Date(2023, 7, 1).toISOString(), endDate: new Date().toISOString(), - activityTypes: [WorkoutActivityType.Pickleball], + activityTypes: [WorkoutActivityType.SwimBikeRun], }); console.log(result); } async function saveWorkout() { + // Workout configurations + const conf1 = { + workoutActivityType: WorkoutActivityType.Swimming, + workoutLocationType: WorkoutSessionLocationType.Outdoor, + workoutSwimmingLocationType: WorkoutSwimmingLocationType.OpenWater, + }; + + const conf2 = { + workoutActivityType: WorkoutActivityType.Running, + workoutLocationType: WorkoutSessionLocationType.Outdoor, + workoutSwimmingLocationType: WorkoutSwimmingLocationType.Unknown, + }; + + // Workout activities + const activity1 = { + workoutConfiguration: conf1, + startDate: new Date(2023, 8, 8, 4, 0).toISOString(), + endDate: new Date(2023, 8, 8, 4, 6).toISOString(), + metadata: null, + }; + + const activity2 = { + workoutConfiguration: conf2, + startDate: new Date(2023, 8, 8, 4, 10).toISOString(), // 5 minutes ago + endDate: new Date(2023, 8, 8, 4, 15).toISOString(), // 1 minute ago + metadata: null, + }; + + // Array of activities + const activities = [activity1, activity2]; + const result = await RNHealthKit.saveWorkout({ - activityType: WorkoutActivityType.Pickleball, + activityType: WorkoutActivityType.SwimBikeRun, startDate: new Date(2023, 8, 8, 4).toISOString(), endDate: new Date(2023, 8, 8, 5).toISOString(), + activities: activities, metadata: { [WorkoutMetadataKey.IndoorWorkout]: false, [WorkoutMetadataKey.FitnessMachineDuration]: { diff --git a/index.ts b/index.ts index c1a6466a..f35f33f9 100644 --- a/index.ts +++ b/index.ts @@ -32,6 +32,7 @@ interface RNHealthKit { totalEnergyBurned?: number; totalDistance?: number; metadata?: WorkoutMetadata; + activities?: WorkoutActivity[] | null; } ): Promise; } @@ -377,11 +378,38 @@ export enum WorkoutActivityType { SocialDance = 78, // Dances done in social settings like swing, salsa and folk dances from different world regions. Pickleball = 79, Cooldown = 80, // Low intensity stretching and mobility exercises following a more vigorous workout type - SwimBikeRun = 81, - Transition = 82, + SwimBikeRun = 82, + Transition = 83, + UnderwaterDiving = 84, Other = 3000, } +export enum WorkoutSessionLocationType { + unknown = 1, + indoor = 2, + outdoor = 3 +} + +export enum WorkoutSwimmingLocationType { + unknown = 0, + pool = 1, + openWater = 2 +} + +export interface WorkoutConfiguration { + workoutActivityType?: WorkoutActivityType | null; + workoutLocationType?: WorkoutSessionLocationType | null; + workoutSwimmingLocationType?: WorkoutSwimmingLocationType | null; + workoutLapLength?: QuantityType | null; +} + +export interface WorkoutActivity { + workoutConfiguration: WorkoutConfiguration; + startDate: Date; + endDate?: Date | null; + metadata?: { [key: string]: any } | null; +} + export enum WorkoutMetadataKey { ActivityType = "HKActivityType", AppleFitnessPlusSession = "HKAppleFitnessPlusSession", From 0d374f30319a2ceae17214675e84af9920e3920d Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:16:06 -0300 Subject: [PATCH 05/14] Remove comments --- example/App.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index 00feaa0c..f66284f4 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -71,7 +71,6 @@ async function runWorkoutQuery() { } async function saveWorkout() { - // Workout configurations const conf1 = { workoutActivityType: WorkoutActivityType.Swimming, workoutLocationType: WorkoutSessionLocationType.Outdoor, @@ -84,7 +83,6 @@ async function saveWorkout() { workoutSwimmingLocationType: WorkoutSwimmingLocationType.Unknown, }; - // Workout activities const activity1 = { workoutConfiguration: conf1, startDate: new Date(2023, 8, 8, 4, 0).toISOString(), @@ -94,12 +92,11 @@ async function saveWorkout() { const activity2 = { workoutConfiguration: conf2, - startDate: new Date(2023, 8, 8, 4, 10).toISOString(), // 5 minutes ago - endDate: new Date(2023, 8, 8, 4, 15).toISOString(), // 1 minute ago + startDate: new Date(2023, 8, 8, 4, 10).toISOString(), + endDate: new Date(2023, 8, 8, 4, 15).toISOString(), metadata: null, }; - // Array of activities const activities = [activity1, activity2]; const result = await RNHealthKit.saveWorkout({ From 0bc9e4e7a6af430f53a88905d760212c0513c715 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:24:55 -0300 Subject: [PATCH 06/14] Update enum cases --- index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index f35f33f9..56be9d5b 100644 --- a/index.ts +++ b/index.ts @@ -385,15 +385,15 @@ export enum WorkoutActivityType { } export enum WorkoutSessionLocationType { - unknown = 1, - indoor = 2, - outdoor = 3 + Unknown = 1, + Indoor = 2, + Outdoor = 3 } export enum WorkoutSwimmingLocationType { - unknown = 0, - pool = 1, - openWater = 2 + Unknown = 0, + Pool = 1, + OpenWater = 2 } export interface WorkoutConfiguration { @@ -441,8 +441,8 @@ export type QuantityType = { } export enum WaterSalinityType { - freshWater = 0, - saltWater = 1, + FreshWater = 0, + SaltWater = 1, } export type WorkoutMetadata = { From 76933797513d26098220fdf8b75f27468924a5e7 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Wed, 29 Nov 2023 19:31:41 -0300 Subject: [PATCH 07/14] Update documentation --- v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift index 4edc4cae..52746b07 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift @@ -18,7 +18,7 @@ public struct WorkoutSample: Encodable { /// The activity type of the workout as a raw unsigned integer value. public let activityType: UInt - // Representing additional activities that are part of the workout. + // Represents additional activities that are part of the workout. public let workoutActivities: [WorkoutActivity]? /// Additional metadata associated with the workout sample. From 97998f1611449737675feb58551ad1d4deaff038 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Thu, 30 Nov 2023 21:30:08 -0300 Subject: [PATCH 08/14] Fix build due to missing dictionary casting --- .../ReactNativeBridge/RNHealthKitWrapper.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift b/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift index f888fefb..eec60bd7 100644 --- a/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift +++ b/v2/RNHealthKitCore/ReactNativeBridge/RNHealthKitWrapper.swift @@ -224,12 +224,12 @@ class RNHealthKitWrapper: NSObject { processedMetadata = try WorkoutHelper.processWorkoutMetadata(metadata) } - let workoutActivities: [WorkoutActivity] - if let activities = workout["activities"] { - let activityData = Data(activities) + var workoutActivities: [WorkoutActivity]? + if let activities = workout["activities"] as? [String: Any] { + let activityData = try JSONSerialization.data(withJSONObject: activities, options: []) workoutActivities = try JSONDecoder().decode([WorkoutActivity].self, from: activityData) } - + if #available(iOS 16.0, *) { try await core?.saveCompletedWorkout( activityType: activityType, From 4ab3b55c699669ec6ce35c5d07b4b91b49653ed2 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Thu, 30 Nov 2023 21:43:41 -0300 Subject: [PATCH 09/14] Fix indentation --- .../Quantity/QuantityQueriesParameters.swift | 9 ++++++++- .../HealthKitCore/Workout/WorkoutQueryParameters.swift | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/v2/RNHealthKitCore/HealthKitCore/Quantity/QuantityQueriesParameters.swift b/v2/RNHealthKitCore/HealthKitCore/Quantity/QuantityQueriesParameters.swift index ddaeec4f..11bff98d 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Quantity/QuantityQueriesParameters.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Quantity/QuantityQueriesParameters.swift @@ -15,7 +15,14 @@ public class QuantityQuery: QueryParameters { /// - isUserEntered: Specifies whether to include/exclude manually entered data. /// - limit: The maximum number of results to retrieve in a query (default is HKObjectQueryNoLimit). /// - unit: The unit of measurement for the queried health data. - public init(startDate: Date?, endDate: Date?, ids: [String]?, isUserEntered: Bool? = nil, limit: Int = HKObjectQueryNoLimit, unit: HKUnit) { + public init( + startDate: Date?, + endDate: Date?, + ids: [String]?, + isUserEntered: Bool? = nil, + limit: Int = HKObjectQueryNoLimit, + unit: HKUnit + ) { self.unit = unit super.init(startDate: startDate, endDate: endDate, isUserEntered: isUserEntered, limit: limit, ids: ids) } diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueryParameters.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueryParameters.swift index 87477613..0e37d6b3 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueryParameters.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutQueryParameters.swift @@ -24,7 +24,14 @@ public class WorkoutQueryParameters: QueryParameters { /// - ids: An array of UUIDs to filter data by specific IDs. /// - isUserEntered: Specifies whether to include/exclude manually entered data. /// - limit: The maximum number of results to retrieve in a query. - public init(startDate: Date? = nil, endDate: Date? = nil, activityTypes: [UInt]? = nil, ids: [String]? = nil, isUserEntered: Bool? = nil, limit: Int = HKObjectQueryNoLimit) { + public init( + startDate: Date? = nil, + endDate: Date? = nil, + activityTypes: [UInt]? = nil, + ids: [String]? = nil, + isUserEntered: Bool? = nil, + limit: Int = HKObjectQueryNoLimit + ) { self.activityTypes = activityTypes super.init(startDate: startDate, endDate: endDate, isUserEntered: isUserEntered, limit: limit, ids: ids) } From f5506036fab00d3abba5161113f871d08286e6c3 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Thu, 30 Nov 2023 21:44:33 -0300 Subject: [PATCH 10/14] Rename Quantity --- v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift | 4 ++-- .../HealthKitCore/Workout/WorkoutConfiguration.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift index c585fbdd..4b5325b3 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift @@ -1,7 +1,7 @@ import Foundation import HealthKit -public struct WorkoutQuantity: Codable { +public struct Quantity: Codable { public let unit: HKUnit public let doubleValue: Double @@ -31,7 +31,7 @@ public struct WorkoutQuantity: Codable { } extension HKQuantity { - public convenience init(quantity: WorkoutQuantity) { + public convenience init(quantity: Quantity) { self.init(unit: quantity.unit, doubleValue: quantity.doubleValue) } } diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift index 01c5a79d..5fb4e5a6 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutConfiguration.swift @@ -5,7 +5,7 @@ public struct WorkoutConfiguration: Codable { public let workoutActivityType: HKWorkoutActivityType? public let workoutLocationType: HKWorkoutSessionLocationType? public let workoutSwimmingLocationType: HKWorkoutSwimmingLocationType? - public let workoutLapLength: WorkoutQuantity? + public let workoutLapLength: Quantity? private enum CodingKeys: String, CodingKey { case workoutActivityType @@ -18,7 +18,7 @@ public struct WorkoutConfiguration: Codable { workoutActivityType: HKWorkoutActivityType? = .other, workoutLocationType: HKWorkoutSessionLocationType = .unknown, workoutSwimmingLocationType: HKWorkoutSwimmingLocationType? = nil, - workoutLapLength: WorkoutQuantity? = nil + workoutLapLength: Quantity? = nil ) { self.workoutActivityType = workoutActivityType self.workoutLocationType = workoutLocationType @@ -39,7 +39,7 @@ public struct WorkoutConfiguration: Codable { let workoutActivityTypeRawValue = try container.decodeIfPresent(Int.self, forKey: .workoutActivityType) let locationTypeRawValue = try container.decodeIfPresent(Int.self, forKey: .workoutLocationType) let swimmingLocationTypeRawValue = try container.decodeIfPresent(Int.self, forKey: .workoutSwimmingLocationType) - let lapLength = try container.decodeIfPresent(WorkoutQuantity.self, forKey: .workoutLapLength) + let lapLength = try container.decodeIfPresent(Quantity.self, forKey: .workoutLapLength) if let rawActivityRawValue = workoutActivityTypeRawValue { self.workoutActivityType = HKWorkoutActivityType(rawValue: UInt(rawActivityRawValue)) From e9126674668f79270cf131eeb456bb48b7d5772d Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Thu, 30 Nov 2023 21:45:19 -0300 Subject: [PATCH 11/14] Add statistics to workout samples --- .../HealthKitCore/HealthKitTypes.swift | 16 +++++++ .../HealthKitCore/Workout/Statistics.swift | 44 +++++++++++++++++++ .../Workout/WorkoutActivity.swift | 2 +- .../HealthKitCore/Workout/WorkoutSample.swift | 19 ++++++-- 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift diff --git a/v2/RNHealthKitCore/HealthKitCore/HealthKitTypes.swift b/v2/RNHealthKitCore/HealthKitCore/HealthKitTypes.swift index 4d628c5f..c3d693e3 100644 --- a/v2/RNHealthKitCore/HealthKitCore/HealthKitTypes.swift +++ b/v2/RNHealthKitCore/HealthKitCore/HealthKitTypes.swift @@ -134,6 +134,22 @@ public enum QuantityType: String, HealthKitType { case HeadphoneAudioExposure // Pressure, DiscreteEquivalentContinuousLevel } +extension QuantityType { + /// Initializes a `QuantityType` with an `HKQuantityType`. + /// + /// - Parameter hkQuantityType: The `HKQuantityType` to convert. + /// - Returns: A corresponding `QuantityType` if a match is found, otherwise `nil`. + public static func from(_ hkQuantityType: HKQuantityType) -> QuantityType? { + let identifier = hkQuantityType.identifier + guard identifier.hasPrefix(hkQuantityTypePrefix) else { return nil } + + let rawValue = String(identifier.dropFirst(hkQuantityTypePrefix.count)) + return QuantityType(rawValue: rawValue) + } +} + +extension QuantityType: Codable {} + public enum WorkoutType: String, HealthKitType { public var type: HKSampleType { return .workoutType() diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift new file mode 100644 index 00000000..a16fc2f6 --- /dev/null +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift @@ -0,0 +1,44 @@ +import Foundation +import HealthKit + +public struct Statistics: Codable { + public let quantityType: QuantityType + public let startDate: String + public let endDate: String + public let averageQuantity: Quantity? + public let minimumQuantity: Quantity? + public let maximumQuantity: Quantity? + public let mostRecentQuantity: Quantity? + public let sumQuantity: Quantity? + public let duration: Double? + + public init?(from hkStatistics: HKStatistics) { + guard let quantityType = QuantityType.from(hkStatistics.quantityType), + let startDate = hkStatistics.startDate.toIsoString(), + let endDate = hkStatistics.endDate.toIsoString() + else { + return nil + } + self.quantityType = quantityType + + self.startDate = startDate + self.endDate = endDate + self.averageQuantity = Self.parseToQuantity(quantityString: hkStatistics.averageQuantity()?.description) + self.minimumQuantity = Self.parseToQuantity(quantityString: hkStatistics.minimumQuantity()?.description) + self.maximumQuantity = Self.parseToQuantity(quantityString: hkStatistics.maximumQuantity()?.description) + self.mostRecentQuantity = Self.parseToQuantity(quantityString: hkStatistics.mostRecentQuantity()?.description) + self.sumQuantity = Self.parseToQuantity(quantityString: hkStatistics.sumQuantity()?.description) + self.duration = hkStatistics.duration()?.doubleValue(for: .second()) + } + + private static func parseToQuantity(quantityString: String?) -> Quantity? { + guard let quantityParts = quantityString?.description.components(separatedBy: " "), + quantityParts.count > 2, + let doubleValue = Double(quantityParts[0]) else { + return nil + } + + let unit = HKUnit(from: quantityParts[1]) + return Quantity(unit: unit, doubleValue: doubleValue) + } +} diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift index 43f0369c..81f637f0 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift @@ -48,7 +48,7 @@ public struct WorkoutActivity: Codable { try container.encode(startDate, forKey: .startDate) try container.encodeIfPresent(endDate, forKey: .endDate) - if let metadata = metadata { + if let metadata { let jsonData = try JSONSerialization.data(withJSONObject: metadata, options: []) try container.encode(jsonData, forKey: .metadata) } diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift index 52746b07..ef6ae8ae 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift @@ -24,8 +24,8 @@ public struct WorkoutSample: Encodable { /// Additional metadata associated with the workout sample. public let metadata: [String: String]? - // TODO: Add workout events | var workoutEvents: [HKWorkoutEvent]? - // TODO: Add statistics public var statistics: [QuantityType : QuantitySample] + /// Represents statistical data associated with the workout. + public let statistics: [QuantityType : Statistics]? /// Initializes a `WorkoutSample` object based on an `HKWorkout` instance. /// @@ -38,9 +38,20 @@ public struct WorkoutSample: Encodable { self.activityType = workout.workoutActivityType.rawValue if #available(iOS 16.0, *) { - workoutActivities = workout.workoutActivities.compactMap({ WorkoutActivity(activity: $0) }) + self.workoutActivities = workout.workoutActivities.compactMap({ WorkoutActivity(activity: $0) }) + self.statistics = workout.allStatistics.compactMap { key, value -> (QuantityType, Statistics)? in + guard let quantityType = QuantityType.from(key), + let statistics = Statistics(from: value) else { + return nil + } + + return (quantityType, statistics) + }.reduce(into: [QuantityType: Statistics]()) { dict, tuple in + dict[tuple.0] = tuple.1 + } } else { - workoutActivities = nil + self.workoutActivities = nil + self.statistics = nil } self.metadata = workout.metadata?.mapValues(String.init(describing:)) ?? [:] } From 2b79930dc6c0f3aede21d24ec2cafcd28e95c479 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Thu, 30 Nov 2023 21:46:44 -0300 Subject: [PATCH 12/14] Update RNCore example app to print HK queries response --- .../HealthKitCore.xcodeproj/project.pbxproj | 4 ++++ .../RNHealthKitCoreApp/ContentView.swift | 17 +++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj b/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj index 0e26297a..068c124a 100644 --- a/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj +++ b/v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ D3785B392B16494A00B8C30A /* WorkoutActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B382B16494A00B8C30A /* WorkoutActivity.swift */; }; D3785B3D2B16558800B8C30A /* Quantity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B3C2B16558800B8C30A /* Quantity.swift */; }; D3785B412B17996E00B8C30A /* WorkoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B402B17996E00B8C30A /* WorkoutConfiguration.swift */; }; + D3785B432B18FF3000B8C30A /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B422B18FF3000B8C30A /* Statistics.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -54,6 +55,7 @@ D3785B382B16494A00B8C30A /* WorkoutActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutActivity.swift; sourceTree = ""; }; D3785B3C2B16558800B8C30A /* Quantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quantity.swift; sourceTree = ""; }; D3785B402B17996E00B8C30A /* WorkoutConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkoutConfiguration.swift; sourceTree = ""; }; + D3785B422B18FF3000B8C30A /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,6 +89,7 @@ D31BF2472B10FC7C00005EE7 /* WorkoutHelper.swift */, D3785B382B16494A00B8C30A /* WorkoutActivity.swift */, D3785B3C2B16558800B8C30A /* Quantity.swift */, + D3785B422B18FF3000B8C30A /* Statistics.swift */, ); path = Workout; sourceTree = ""; @@ -202,6 +205,7 @@ 7E4BFFE82AAA421200FB1383 /* WorkoutQueryParameters.swift in Sources */, 7E5AEBB12A5EA8EA00F74829 /* QuantitySample.swift in Sources */, 7E4BFFE62A9FC0ED00FB1383 /* WorkoutQueries.swift in Sources */, + D3785B432B18FF3000B8C30A /* Statistics.swift in Sources */, 7E9A84F72A548A39004622E5 /* Utils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift b/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift index e5a30cb8..4e7f5754 100644 --- a/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift +++ b/v2/RNHealthKitCoreApp/RNHealthKitCoreApp/ContentView.swift @@ -3,12 +3,11 @@ import HealthKitCore import HealthKit struct ContentView: View { + @State private var workoutDetails: String = "" + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") + ScrollView { + Text(workoutDetails) } .padding() .onAppear { @@ -19,8 +18,8 @@ struct ContentView: View { write: [WorkoutType.workout] ) if HKHealthStore.isHealthDataAvailable() { -// await saveWorkoutWithMetadata(core: core) - dump(try await core.getCompletedWorkouts(queryParameters: .init())) +// try await saveWorkoutWithMetadata(core: core) + workoutDetails = (try await core.getCompletedWorkouts(queryParameters: .init(startDate: Calendar.current.date(byAdding: .day, value: -15, to: .now)!, endDate: .now))).debugDescription } } catch { print("Error occurred: \(error)") @@ -65,7 +64,9 @@ struct ContentView: View { totalEnergyBurned: 120, totalDistance: 2000, activities: [activity1, activity2], - metadata: nil + metadata: [ + "Testing": "1235" + ] )) } else { print(try await core.saveCompletedWorkout( From 20a760506dfff895944a86b2c27d0825df6e4e61 Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Thu, 30 Nov 2023 23:28:42 -0300 Subject: [PATCH 13/14] Update workout sample --- .../HealthKitCore/Workout/WorkoutSample.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift index ef6ae8ae..4d17c650 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutSample.swift @@ -25,7 +25,7 @@ public struct WorkoutSample: Encodable { public let metadata: [String: String]? /// Represents statistical data associated with the workout. - public let statistics: [QuantityType : Statistics]? + public let statistics: [Statistics]? /// Initializes a `WorkoutSample` object based on an `HKWorkout` instance. /// @@ -39,15 +39,11 @@ public struct WorkoutSample: Encodable { if #available(iOS 16.0, *) { self.workoutActivities = workout.workoutActivities.compactMap({ WorkoutActivity(activity: $0) }) - self.statistics = workout.allStatistics.compactMap { key, value -> (QuantityType, Statistics)? in - guard let quantityType = QuantityType.from(key), - let statistics = Statistics(from: value) else { + self.statistics = workout.allStatistics.compactMap { key, value -> Statistics? in + guard let statistics = Statistics(from: value) else { return nil } - - return (quantityType, statistics) - }.reduce(into: [QuantityType: Statistics]()) { dict, tuple in - dict[tuple.0] = tuple.1 + return statistics } } else { self.workoutActivities = nil From 2f9691331de8ecd13d42e89b17b7156748825cff Mon Sep 17 00:00:00 2001 From: Thalys Viana Date: Fri, 1 Dec 2023 11:51:50 -0300 Subject: [PATCH 14/14] Fix logic on parseToQuantity method --- v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift b/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift index a16fc2f6..5f6b459e 100644 --- a/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift +++ b/v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift @@ -32,8 +32,8 @@ public struct Statistics: Codable { } private static func parseToQuantity(quantityString: String?) -> Quantity? { - guard let quantityParts = quantityString?.description.components(separatedBy: " "), - quantityParts.count > 2, + guard let quantityParts = quantityString?.components(separatedBy: " "), + quantityParts.count == 2, let doubleValue = Double(quantityParts[0]) else { return nil }