From 799d3f2ed4298be871a5a361b040101ac37abe14 Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 9 Feb 2023 17:29:42 +0200 Subject: [PATCH] feat(ios): add HealthKit.queryAnchoredWorkouts method --- ios/RNHealthTracker.m | 6 ++ ios/RNHealthTracker.swift | 91 ++++++++++++++++++++++ src/api/healthKit/index.ts | 1 + src/api/healthKit/queryAnchoredWorkouts.ts | 32 ++++++++ src/types/healthKitTypes.ts | 25 +++--- 5 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 src/api/healthKit/queryAnchoredWorkouts.ts diff --git a/ios/RNHealthTracker.m b/ios/RNHealthTracker.m index 338c3e6..b73f76c 100644 --- a/ios/RNHealthTracker.m +++ b/ios/RNHealthTracker.m @@ -73,6 +73,12 @@ @interface RCT_EXTERN_MODULE (RNHealthTracker, NSObject) metadata:(NSDictionary*)metadata resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(anchoredQueryWorkouts: + (nonnull NSNumber*)workoutWithActivityType + lastAnchor:(nonnull NSNumber*)lastAnchor + limit:(nonnull NSNumber*)limit + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(queryWorkouts: (nonnull NSNumber*)workoutWithActivityType start:(nonnull NSNumber*)start diff --git a/ios/RNHealthTracker.swift b/ios/RNHealthTracker.swift index ad229ac..881f119 100644 --- a/ios/RNHealthTracker.swift +++ b/ios/RNHealthTracker.swift @@ -778,6 +778,97 @@ class RNHealthTracker: NSObject { } } + @objc public func anchoredQueryWorkouts( + _ workoutActivityType: NSNumber, + lastAnchor: NSNumber, + limit: NSNumber, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + var predicate: NSPredicate? = nil + var anchor: HKQueryAnchor? = nil + + if workoutActivityType.uintValue > 0 { + guard let workoutType = HKWorkoutActivityType.init(rawValue: workoutActivityType.uintValue) else { + return handleError(reject: reject, code: 1, description: "Invalid workoutActivityType.") + } + predicate = HKQuery.predicateForWorkouts(with: workoutType) + } + + if lastAnchor.intValue != 0 { + anchor = HKQueryAnchor(fromValue: lastAnchor.intValue) + } + + let query: HKAnchoredObjectQuery = HKAnchoredObjectQuery.init( + type: HKWorkoutType.workoutType(), + predicate: predicate, + anchor: anchor, + limit: limit.intValue == 0 ? HKObjectQueryNoLimit : limit.intValue) + { (query, samplesOrNil, deletedObjectsOrNil, newAnchor, errorOrNil) in + if let error = errorOrNil { + return self.handleError(reject: reject, code: 2, error: error) + } + + var newRecords: [Dictionary] = [] + var deletedRecords: [Dictionary] = [] + + guard let samples = samplesOrNil as? [HKWorkout] else { + return self.handleError(reject: reject, description: "Error getting samples as HKWorkout") + } + + for sample in samples { + + let workout: HKWorkout = sample + + let sourceDevice: String = sample.sourceRevision.productType ?? "unknown" + + let distance: Double? = workout.totalDistance?.doubleValue(for: .meter()) + let energyBurned: Double? = workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) + let isoStartDate = RNFitnessUtils.formatUtcIsoDateTimeString(workout.startDate) + let isoEndDate = RNFitnessUtils.formatUtcIsoDateTimeString(workout.endDate) + + newRecords.append([ + "uuid": workout.uuid.uuidString, + "duration": workout.duration, + "startDate": isoStartDate, + "endDate": isoEndDate, + "energyBurned": energyBurned, + "distance": distance, + "type": workout.workoutActivityType.rawValue, + "metadata": workout.metadata, + "source": [ + "name": workout.sourceRevision.source.name, + "device": sourceDevice, + "id": workout.sourceRevision.source.bundleIdentifier, + ] + ]) + } + + guard let deletedObjects = deletedObjectsOrNil else { + return self.handleError(reject: reject, description: "Error getting deletedObjects for anchored querry") + } + + for deletedSample in deletedObjects { + deletedRecords.append([ + "uuid": deletedSample.uuid.uuidString, + "metadata": deletedSample.metadata, + ]) + } + + resolve([ + "anchor": newAnchor?.value(forKey: "rowid"), + "deletedRecords": deletedRecords, + "newRecords": newRecords + ]); + } + + healthStore.execute(query) + + DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { + self.healthStore.stop(query) + } + } + @objc public func queryWorkouts( _ workoutActivityType: NSNumber, start: NSNumber, diff --git a/src/api/healthKit/index.ts b/src/api/healthKit/index.ts index 32d32a9..978723b 100644 --- a/src/api/healthKit/index.ts +++ b/src/api/healthKit/index.ts @@ -11,6 +11,7 @@ export * from './getStatisticTotalForToday'; export * from './getStatisticTotalForWeek'; export * from './getStatisticWeekDaily'; export * from './isHealthDataAvailable'; +export * from './queryAnchoredWorkouts'; export * from './queryDailyTotals'; export * from './queryDataRecords'; export * from './queryTotal'; diff --git a/src/api/healthKit/queryAnchoredWorkouts.ts b/src/api/healthKit/queryAnchoredWorkouts.ts new file mode 100644 index 0000000..5715c57 --- /dev/null +++ b/src/api/healthKit/queryAnchoredWorkouts.ts @@ -0,0 +1,32 @@ +import { NativeModules } from 'react-native'; + +import { HealthKitWorkoutType } from '../../enums'; +import { HealthKitAnchoredWorkoutResult } from '../../types'; +import { isIOS } from '../../utils'; + +/** @internal */ +const { RNHealthTracker } = NativeModules; + +/** + * Query workouts with anchor + * Passing anchor will return all workouts that have been added since that anchor point + * + * @param options.anchor last query anchor point + * @param options.key e.g. `HealthKitWorkoutType.Running`HealthKit + * @param options.limit limits the number of workouts returned from the anchor point to the newest workout + * + * @return Returns an object with the latest anchor point and data array with new workouts + */ +export const queryAnchoredWorkouts = async (options?: { + anchor?: number; + key?: HealthKitWorkoutType; + limit?: number; +}): Promise => { + if (isIOS) { + const { anchor = 0, key = 0, limit = 0 } = options || {}; + + return RNHealthTracker.anchoredQueryWorkouts(key, anchor, limit); + } + + throw new Error('queryAnchoredWorkouts is implemented only for iOS platform'); +}; diff --git a/src/types/healthKitTypes.ts b/src/types/healthKitTypes.ts index 368fb4a..e441b79 100644 --- a/src/types/healthKitTypes.ts +++ b/src/types/healthKitTypes.ts @@ -4,6 +4,9 @@ import { HealthKitWorkoutType, } from '../enums'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type HealthKitMetadata = { [key: string]: any } | null; + export interface HealthWorkoutRecord { uuid: string; duration: number; @@ -12,8 +15,7 @@ export interface HealthWorkoutRecord { startDate: string; endDate: string; type: HealthKitWorkoutType | 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - metadata: { [name: string]: any }; + metadata: HealthKitMetadata; source: { name: string; device: string; @@ -23,11 +25,16 @@ export interface HealthWorkoutRecord { export type HealthWorkoutRecordQuery = Array; +export interface HealthKitAnchoredWorkoutResult { + anchor: number; + newRecords: HealthWorkoutRecordQuery; + deletedRecords: HealthKitDeletedWorkoutRecord; +} + export interface HealthDataRecord { date: string; quantity: number; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - metadata: { [name: string]: any } | null; + metadata: HealthKitMetadata; source: { device: string; id: string; @@ -37,6 +44,11 @@ export interface HealthDataRecord { uuid: string; } +export interface HealthKitDeletedWorkoutRecord { + uuid: string; + metadata: HealthKitMetadata; +} + export type HealthDataRecordQuery = Array; export interface HealthKitKeyWithUnit { @@ -44,11 +56,6 @@ export interface HealthKitKeyWithUnit { unit: HealthKitUnitType; } -export interface HealthKitMetadata { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - export enum HealthKitAuthStatus { /** * Authorization is not determined. Usually means permissions not requested.