Skip to content

Commit

Permalink
Add queryStatisticsCollectionForQuantity
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyang committed Oct 31, 2023
1 parent 2e94f73 commit 540463b
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 17 deletions.
10 changes: 10 additions & 0 deletions ios/ReactNativeHealthkit.m
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ @interface RCT_EXTERN_MODULE(ReactNativeHealthkit, RCTEventEmitter)
reject:(RCTPromiseRejectBlock)reject
)

RCT_EXTERN_METHOD(queryStatisticsCollectionForQuantity:(NSString)typeIdentifier
unitString:(NSString)unitString
options:(NSArray)options
anchorDate:(NSDate)anchorDate
interval:(NSDictionary)interval
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
)


RCT_EXTERN_METHOD(getWheelchairUse:(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)

Expand Down
128 changes: 111 additions & 17 deletions ios/ReactNativeHealthkit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -627,19 +627,7 @@ class ReactNativeHealthkit: RCTEventEmitter {
return true
}

@objc(queryStatisticsForQuantity:unitString:from:to:options:resolve:reject:)
func queryStatisticsForQuantity(typeIdentifier: String, unitString: String, from: Date, to: Date, options: NSArray, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let store = _store else {
return reject(INIT_ERROR, INIT_ERROR_MESSAGE, nil)
}

let identifier = HKQuantityTypeIdentifier.init(rawValue: typeIdentifier)
guard let quantityType = HKObjectType.quantityType(forIdentifier: identifier) else {
return reject(TYPE_IDENTIFIER_ERROR, "Failed to initialize " + typeIdentifier, nil)
}

let predicate = HKQuery.predicateForSamples(withStart: from, end: to, options: HKQueryOptions.strictEndDate)

fileprivate func convertOptionsToHKStatisticsOptions(_ options: NSArray) -> HKStatisticsOptions {
var opts = HKStatisticsOptions.init()

for o in options {
Expand All @@ -654,9 +642,9 @@ class ReactNativeHealthkit: RCTEventEmitter {
opts.insert(HKStatisticsOptions.discreteMin)
}
if #available(iOS 12, *) {
if str == "discreteMostRecent" {
opts.insert(HKStatisticsOptions.discreteMostRecent)
}
if str == "discreteMostRecent" {
opts.insert(HKStatisticsOptions.discreteMostRecent)
}
}
if #available(iOS 13, *) {
if str == "duration" {
Expand All @@ -666,11 +654,28 @@ class ReactNativeHealthkit: RCTEventEmitter {
opts.insert(HKStatisticsOptions.mostRecent)
}
}

if str == "separateBySource" {
opts.insert(HKStatisticsOptions.separateBySource)
}
}
return opts
}

@objc(queryStatisticsForQuantity:unitString:from:to:options:resolve:reject:)
func queryStatisticsForQuantity(typeIdentifier: String, unitString: String, from: Date, to: Date, options: NSArray, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let store = _store else {
return reject(INIT_ERROR, INIT_ERROR_MESSAGE, nil)
}

let identifier = HKQuantityTypeIdentifier.init(rawValue: typeIdentifier)
guard let quantityType = HKObjectType.quantityType(forIdentifier: identifier) else {
return reject(TYPE_IDENTIFIER_ERROR, "Failed to initialize " + typeIdentifier, nil)
}

let predicate = HKQuery.predicateForSamples(withStart: from, end: to, options: HKQueryOptions.strictEndDate)

let opts = convertOptionsToHKStatisticsOptions(options)

let query = HKStatisticsQuery.init(quantityType: quantityType, quantitySamplePredicate: predicate, options: opts) { (_, stats: HKStatistics?, _: Error?) in
var dic = [String: [String: Any]?]()
Expand Down Expand Up @@ -717,6 +722,95 @@ class ReactNativeHealthkit: RCTEventEmitter {
store.execute(query)
}

@objc(queryStatisticsCollectionForQuantity:unitString:options:anchorDate:interval:resolve:reject:)
func queryStatisticsCollectionForQuantity(typeIdentifier: String, unitString: String, options: NSArray, anchorDate: Date, interval: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let store = _store else {
return reject(INIT_ERROR, INIT_ERROR_MESSAGE, nil)
}

let identifier = HKQuantityTypeIdentifier.init(rawValue: typeIdentifier)
guard let quantityType = HKObjectType.quantityType(forIdentifier: identifier) else {
return reject(TYPE_IDENTIFIER_ERROR, "Failed to initialize " + typeIdentifier, nil)
}

let opts = convertOptionsToHKStatisticsOptions(options)

// Set the interval object based on JS Object
let componentKeys: [String: WritableKeyPath<DateComponents, Int?>] = [
"day": \.day,
"month": \.month,
"year": \.year,
"hour": \.hour,
"minute": \.minute
]

var intervalComponents = DateComponents()
for (key, keyPath) in componentKeys {
if let value = interval[key] as? Int {
intervalComponents[keyPath: keyPath] = value
}
}


let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: nil, options: opts, anchorDate: anchorDate, intervalComponents: intervalComponents)

query.initialResultsHandler = { query, statsCollection, error in
var results = [[String: [String: Any]?]]()

guard let collection = statsCollection else {
return resolve(results)
}

let statsArray = collection.statistics()
let unit = HKUnit.init(from: unitString)
for gottenStats in statsArray {
var dic = [String: [String: Any]?]()

let startDate = self._dateFormatter.string(from: gottenStats.startDate)
let endDate = self._dateFormatter.string(from: gottenStats.endDate)
let quantityType = gottenStats.quantityType

if let averageQuantity = gottenStats.averageQuantity() {
dic.updateValue(serializeQuantity(unit: unit, quantity: averageQuantity), forKey: "averageQuantity")
}
if let maximumQuantity = gottenStats.maximumQuantity() {
dic.updateValue(serializeQuantity(unit: unit, quantity: maximumQuantity), forKey: "maximumQuantity")
}
if let minimumQuantity = gottenStats.minimumQuantity() {
dic.updateValue(serializeQuantity(unit: unit, quantity: minimumQuantity), forKey: "minimumQuantity")
}
if let sumQuantity = gottenStats.sumQuantity() {
dic.updateValue(serializeStatistic(unit: unit, quantity: sumQuantity, stats: gottenStats), forKey: "sumQuantity")
}
if #available(iOS 12, *) {
if let mostRecent = gottenStats.mostRecentQuantity() {
dic.updateValue(serializeQuantity(unit: unit, quantity: mostRecent), forKey: "mostRecentQuantity")
}

if let mostRecentDateInterval = gottenStats.mostRecentQuantityDateInterval() {
dic.updateValue([
"start": self._dateFormatter.string(from: mostRecentDateInterval.start),
"end": self._dateFormatter.string(from: mostRecentDateInterval.end)
], forKey: "mostRecentQuantityDateInterval")
}
}
if #available(iOS 13, *) {
let durationUnit = HKUnit.second()
if let duration = gottenStats.duration() {
dic.updateValue(serializeQuantity(unit: durationUnit, quantity: duration), forKey: "duration")
}
}
results.append(dic)
}

resolve(results)

}

store.execute(query)
}


@objc(queryWorkoutSamples:distanceUnitString:from:to:limit:ascending:resolve:reject:)
func queryWorkoutSamples(energyUnitString: String, distanceUnitString: String, from: Date, to: Date, limit: Int, ascending: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let store = _store else {
Expand Down
22 changes: 22 additions & 0 deletions ios/Serializers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ func serializeQuantity(unit: HKUnit, quantity: HKQuantity?) -> [String: Any]? {
]
}

func serializeStatistic(unit: HKUnit, quantity: HKQuantity?, stats: HKStatistics?) -> [String: Any]? {
guard let q = quantity else {
return nil
}

guard let stats = stats else {
return nil
}

let endDate = _dateFormatter.string(from: stats.endDate)
let startDate = _dateFormatter.string(from: stats.startDate)
let quantityType = stats.quantityType.identifier

return [
"unit": unit.unitString,
"quantity": q.doubleValue(for: unit),
"endDate": endDate,
"startDate": startDate,
"quantityType": quantityType
]
}

func serializeQuantitySample(sample: HKQuantitySample, unit: HKUnit) -> NSDictionary {
let endDate = _dateFormatter.string(from: sample.endDate)
let startDate = _dateFormatter.string(from: sample.startDate)
Expand Down
3 changes: 3 additions & 0 deletions src/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import queryQuantitySamples from './utils/queryQuantitySamples'
import queryQuantitySamplesWithAnchor from './utils/queryQuantitySamplesWithAnchor'
import querySources from './utils/querySources'
import queryStatisticsForQuantity from './utils/queryStatisticsForQuantity'
import queryStatisticsCollectionForQuantity from './utils/queryStatisticsCollectionForQuantity'
import queryWorkouts from './utils/queryWorkouts'
import requestAuthorization from './utils/requestAuthorization'
import saveCategorySample from './utils/saveCategorySample'
Expand Down Expand Up @@ -168,6 +169,7 @@ export default {
queryQuantitySamples,
queryQuantitySamplesWithAnchor,
queryStatisticsForQuantity,
queryStatisticsCollectionForQuantity,
queryWorkouts,
querySources,

Expand Down Expand Up @@ -249,6 +251,7 @@ export {
queryQuantitySamples,
queryQuantitySamplesWithAnchor,
queryStatisticsForQuantity,
queryStatisticsCollectionForQuantity,
queryWorkouts,
querySources,
requestAuthorization,
Expand Down
11 changes: 11 additions & 0 deletions src/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ const authorizationStatusFor = UnavailableFn(Promise.resolve(HKAuthorizationStat
mostRecentQuantityDateInterval: undefined,
duration: undefined,
})),
queryStatisticsCollectionForQuantity = UnavailableFn(Promise.resolve({
averageQuantity: undefined,
maximumQuantity: undefined,
minimumQuantity: undefined,
sumQuantity: undefined,
mostRecentQuantity: undefined,
mostRecentQuantityDateInterval: undefined,
duration: undefined,
})),
queryWorkouts = UnavailableFn(Promise.resolve([])),
querySources = UnavailableFn(Promise.resolve([])),
requestAuthorization = UnavailableFn(Promise.resolve(false)),
Expand Down Expand Up @@ -123,6 +132,7 @@ const Healthkit: typeof ReactNativeHealthkit = {
queryQuantitySamples,
queryQuantitySamplesWithAnchor,
queryStatisticsForQuantity,
queryStatisticsCollectionForQuantity,
queryWorkouts,
querySources,
requestAuthorization,
Expand Down Expand Up @@ -174,6 +184,7 @@ export {
queryQuantitySamples,
queryQuantitySamplesWithAnchor,
queryStatisticsForQuantity,
queryStatisticsCollectionForQuantity,
queryWorkouts,
querySources,
requestAuthorization,
Expand Down
1 change: 1 addition & 0 deletions src/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const mockModule: NativeModule & typeof Native = {
queryQuantitySamples: jest.fn(),
querySources: jest.fn(),
queryStatisticsForQuantity: jest.fn(),
queryStatisticsCollectionForQuantity: jest.fn(),
queryWorkoutSamples: jest.fn(),
saveCategorySample: jest.fn(),
saveCorrelationSample: jest.fn(),
Expand Down
13 changes: 13 additions & 0 deletions src/native-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,8 @@ export type QueryStatisticsResponseRaw<
readonly duration?: HKQuantity<HKQuantityTypeIdentifier, TimeUnit>;
};



/**
* @see {@link https://developer.apple.com/documentation/healthkit/hkcategoryvaluecervicalmucusquality Apple Docs }
*/
Expand Down Expand Up @@ -2097,6 +2099,17 @@ type ReactNativeHealthkitTypeNative = {
to: string,
options: readonly HKStatisticsOptions[]
) => Promise<QueryStatisticsResponseRaw<TIdentifier, TUnit>>;

readonly queryStatisticsCollectionForQuantity: <
TIdentifier extends HKQuantityTypeIdentifier,
TUnit extends UnitForIdentifier<TIdentifier>
>(
identifier: HKQuantityTypeIdentifier,
unit: TUnit,
options: readonly HKStatisticsOptions[],
anchorDate: string,
intervalComponents: any
) => Promise<QueryStatisticsResponseRaw<TIdentifier, TUnit>>;
readonly getPreferredUnits: (
identifiers: readonly HKQuantityTypeIdentifier[]
) => Promise<TypeToUnitMapping>;
Expand Down
25 changes: 25 additions & 0 deletions src/utils/queryStatisticsCollectionForQuantity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ensureUnit from './ensureUnit'
import Native from '../native-types'

import type { HKQuantityTypeIdentifier, HKStatisticsOptions, UnitForIdentifier } from '../native-types'

async function queryStatisticsCollectionForQuantity<TIdentifier extends HKQuantityTypeIdentifier, TUnit extends UnitForIdentifier<TIdentifier> = UnitForIdentifier<TIdentifier>>(
identifier: TIdentifier,
options: readonly HKStatisticsOptions[],
anchorDate: Date,
intervalComponents: any,
unit?: TUnit,
) {
const actualUnit = await ensureUnit(identifier, unit)
const rawResponse = await Native.queryStatisticsCollectionForQuantity(
identifier,
actualUnit,
options,
anchorDate.toISOString(),
intervalComponents
)

return rawResponse
}

export default queryStatisticsCollectionForQuantity

0 comments on commit 540463b

Please sign in to comment.