Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add statistics info to workout samples #353

Draft
wants to merge 14 commits into
base: swift
Choose a base branch
from
35 changes: 33 additions & 2 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import RNHealthKit, {
Interval,
WorkoutActivityType,
WorkoutMetadataKey,
WorkoutSessionLocationType,
WorkoutSwimmingLocationType,
} from 'react-native-health';

RNHealthKit.initHealthKit(
Expand Down Expand Up @@ -63,16 +65,45 @@ 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() {
const conf1 = {
workoutActivityType: WorkoutActivityType.Swimming,
workoutLocationType: WorkoutSessionLocationType.Outdoor,
workoutSwimmingLocationType: WorkoutSwimmingLocationType.OpenWater,
};

const conf2 = {
workoutActivityType: WorkoutActivityType.Running,
workoutLocationType: WorkoutSessionLocationType.Outdoor,
workoutSwimmingLocationType: WorkoutSwimmingLocationType.Unknown,
};

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(),
endDate: new Date(2023, 8, 8, 4, 15).toISOString(),
metadata: null,
};

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]: {
Expand Down
36 changes: 32 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface RNHealthKit {
totalEnergyBurned?: number;
totalDistance?: number;
metadata?: WorkoutMetadata;
activities?: WorkoutActivity[] | null;
}
): Promise<boolean>;
}
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -413,8 +441,8 @@ export type QuantityType = {
}

export enum WaterSalinityType {
freshWater = 0,
saltWater = 1,
FreshWater = 0,
SaltWater = 1,
}

export type WorkoutMetadata = {
Expand Down
16 changes: 16 additions & 0 deletions v2/RNHealthKitCore/HealthKitCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
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 */; };
D3785B432B18FF3000B8C30A /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3785B422B18FF3000B8C30A /* Statistics.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand Down Expand Up @@ -48,6 +52,10 @@
7EB70C892A61ABAF003EE217 /* RNHealthKitWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNHealthKitWrapper.m; sourceTree = "<group>"; };
7EE4AA502A669CA200CC9EEF /* RNHealthKitWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNHealthKitWrapper.swift; sourceTree = "<group>"; };
D31BF2472B10FC7C00005EE7 /* WorkoutHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutHelper.swift; sourceTree = "<group>"; };
D3785B382B16494A00B8C30A /* WorkoutActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutActivity.swift; sourceTree = "<group>"; };
D3785B3C2B16558800B8C30A /* Quantity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quantity.swift; sourceTree = "<group>"; };
D3785B402B17996E00B8C30A /* WorkoutConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkoutConfiguration.swift; sourceTree = "<group>"; };
D3785B422B18FF3000B8C30A /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -74,10 +82,14 @@
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 */,
D3785B422B18FF3000B8C30A /* Statistics.swift */,
);
path = Workout;
sourceTree = "<group>";
Expand Down Expand Up @@ -183,13 +195,17 @@
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 */,
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;
Expand Down
16 changes: 16 additions & 0 deletions v2/RNHealthKitCore/HealthKitCore/HealthKitTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
37 changes: 37 additions & 0 deletions v2/RNHealthKitCore/HealthKitCore/Workout/Quantity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import HealthKit

public struct Quantity: 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: Quantity) {
self.init(unit: quantity.unit, doubleValue: quantity.doubleValue)
}
}
44 changes: 44 additions & 0 deletions v2/RNHealthKitCore/HealthKitCore/Workout/Statistics.swift
Original file line number Diff line number Diff line change
@@ -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?.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)
}
}
69 changes: 69 additions & 0 deletions v2/RNHealthKitCore/HealthKitCore/Workout/WorkoutActivity.swift
Original file line number Diff line number Diff line change
@@ -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 {
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
}
}
}
Loading