Skip to content

Commit

Permalink
feat(ios): add HealthKit.queryAnchoredWorkouts method
Browse files Browse the repository at this point in the history
  • Loading branch information
alarm109 committed Feb 9, 2023
1 parent e4461f5 commit 799d3f2
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 9 deletions.
6 changes: 6 additions & 0 deletions ios/RNHealthTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions ios/RNHealthTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any?>] = []
var deletedRecords: [Dictionary<String, Any?>] = []

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,
Expand Down
1 change: 1 addition & 0 deletions src/api/healthKit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
32 changes: 32 additions & 0 deletions src/api/healthKit/queryAnchoredWorkouts.ts
Original file line number Diff line number Diff line change
@@ -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<HealthKitAnchoredWorkoutResult> => {
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');
};
25 changes: 16 additions & 9 deletions src/types/healthKitTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -23,11 +25,16 @@ export interface HealthWorkoutRecord {

export type HealthWorkoutRecordQuery = Array<HealthWorkoutRecord>;

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;
Expand All @@ -37,18 +44,18 @@ export interface HealthDataRecord {
uuid: string;
}

export interface HealthKitDeletedWorkoutRecord {
uuid: string;
metadata: HealthKitMetadata;
}

export type HealthDataRecordQuery = Array<HealthDataRecord>;

export interface HealthKitKeyWithUnit {
key: HealthKitDataType;
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.
Expand Down

0 comments on commit 799d3f2

Please sign in to comment.