Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-pandas-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kingstinct/react-native-healthkit": minor
---

Add `queryActivitySummaries(startDate, endDate)` exposing `HKActivitySummaryQuery` — per-day activity ring values and goals (move, exercise, stand)
51 changes: 51 additions & 0 deletions packages/react-native-healthkit/ios/ActivitySummaryModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import HealthKit
import NitroModules

class ActivitySummaryModule: HybridActivitySummaryModuleSpec {
func queryActivitySummaries(startDate: Date, endDate: Date) throws -> Promise<[ActivitySummary]> {
return Promise.async {
let calendar = Calendar.current

var startComponents = calendar.dateComponents(
[.day, .month, .year, .era], from: startDate)
var endComponents = calendar.dateComponents(
[.day, .month, .year, .era], from: endDate)
Comment on lines +6 to +12
// HKActivitySummaryQuery requires the calendar to be set on the predicate's
// DateComponents — without it the query returns no results.
startComponents.calendar = calendar
endComponents.calendar = calendar

let predicate = HKQuery.predicate(
forActivitySummariesBetweenStart: startComponents, end: endComponents)

let summaries: [HKActivitySummary] = try await withCheckedThrowingContinuation {
continuation in
let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: summaries ?? [])
}
}
Comment on lines +23 to +29
store.execute(query)
}

return summaries.map { summary in
let components = summary.dateComponents(for: calendar)
return ActivitySummary(
dateYear: Double(components.year ?? 0),
dateMonth: Double(components.month ?? 0),
dateDay: Double(components.day ?? 0),
activeEnergyBurned: summary.activeEnergyBurned.doubleValue(for: .kilocalorie()),
activeEnergyBurnedGoal: summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()),
appleExerciseTime: summary.appleExerciseTime.doubleValue(for: .minute()),
appleExerciseTimeGoal: summary.appleExerciseTimeGoal.doubleValue(for: .minute()),
appleStandHours: summary.appleStandHours.doubleValue(for: .count()),
appleStandHoursGoal: summary.appleStandHoursGoal.doubleValue(for: .count()),
appleMoveTime: summary.appleMoveTime.doubleValue(for: .minute()),
appleMoveTimeGoal: summary.appleMoveTimeGoal.doubleValue(for: .minute())
)
}
Comment on lines +33 to +48
}
}
}
3 changes: 3 additions & 0 deletions packages/react-native-healthkit/nitro.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
},
"MedicationModule": {
"swift": "MedicationModule"
},
"ActivitySummaryModule": {
"swift": "ActivitySummaryModule"
}
},
"ignorePaths": ["**/node_modules"]
Expand Down
19 changes: 19 additions & 0 deletions packages/react-native-healthkit/src/healthkit.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useSubscribeToCategorySamples from './hooks/useSubscribeToCategorySamples
import useSubscribeToChanges from './hooks/useSubscribeToChanges'
import useSubscribeToQuantitySamples from './hooks/useSubscribeToQuantitySamples'
import {
ActivitySummaries,
CategoryTypes,
Characteristics,
Core,
Expand Down Expand Up @@ -38,6 +39,7 @@ import type {
MedicationDoseEventTyped,
} from './types/Medication'
import type { QuantityTypeIdentifier } from './types/QuantityTypeIdentifier'
import type { ActivitySummary } from './types/ActivitySummary'
import type {
StateOfMindSamplesWithAnchorResponseTyped,
StateOfMindSampleTyped,
Expand Down Expand Up @@ -296,6 +298,19 @@ const MedicationBindings = {
>
}

const ActivitySummaryBindings = {
queryActivitySummaries: bindRetypedMethod<
typeof ActivitySummaries,
typeof ActivitySummaries.queryActivitySummaries,
Promise<ActivitySummary[]>
>(ActivitySummaries, ActivitySummaries.queryActivitySummaries),
} satisfies {
queryActivitySummaries: BoundMethod<
typeof ActivitySummaries.queryActivitySummaries,
Promise<ActivitySummary[]>
>
}
Comment on lines +301 to +312

// Named exports - all functions bound to their respective modules
export const authorizationStatusFor = Core.authorizationStatusFor.bind(Core)
export const requestPerObjectReadAuthorization =
Expand Down Expand Up @@ -390,6 +405,9 @@ export const queryMedicationEvents = MedicationBindings.queryMedicationEvents
export const queryMedicationEventsWithAnchor =
MedicationBindings.queryMedicationEventsWithAnchor

export const queryActivitySummaries =
ActivitySummaryBindings.queryActivitySummaries

export const currentAppSource = Core.currentAppSource.bind(Core)

export const getBiologicalSexAsync =
Expand Down Expand Up @@ -461,6 +479,7 @@ export default {
subscribeToQuantitySamples,
startWatchApp,
isProtectedDataAvailable,
queryActivitySummaries,
queryStateOfMindSamples,
queryStateOfMindSamplesWithAnchor,
saveStateOfMindSample,
Expand Down
7 changes: 7 additions & 0 deletions packages/react-native-healthkit/src/healthkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,12 @@ export const startWatchApp = UnavailableFnFromModule(
Promise.resolve(false),
)

// ActivitySummaryModule functions
export const queryActivitySummaries = UnavailableFnFromModule(
'queryActivitySummaries',
Promise.resolve([]),
)

// StateOfMindModule functions
export const queryStateOfMindSamples = UnavailableFnFromModule(
'queryStateOfMindSamples',
Expand Down Expand Up @@ -630,6 +636,7 @@ const HealthkitModule = {
subscribeToChanges,
startWatchApp,
isProtectedDataAvailable,
queryActivitySummaries,
queryStateOfMindSamples,
queryStateOfMindSamplesWithAnchor,
saveStateOfMindSample,
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native-healthkit/src/modules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NitroModules } from 'react-native-nitro-modules'
import type { ActivitySummaryModule } from './specs/ActivitySummaryModule.nitro'
import type {
CategoryTypeModule,
CategoryTypeModuleTyped,
Expand Down Expand Up @@ -56,3 +57,8 @@ export const HeartbeatSeries =

export const StateOfMind =
NitroModules.createHybridObject<StateOfMindModule>('StateOfMindModule')

export const ActivitySummaries =
NitroModules.createHybridObject<ActivitySummaryModule>(
'ActivitySummaryModule',
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { HybridObject } from 'react-native-nitro-modules'
import type { ActivitySummary } from '../types/ActivitySummary'

export interface ActivitySummaryModule extends HybridObject<{ ios: 'swift' }> {
/**
* Query HKActivitySummary rollups for the calendar days intersecting
* [startDate, endDate] (inclusive, interpreted in the user's current calendar).
*/
queryActivitySummaries(
startDate: Date,
endDate: Date,
): Promise<ActivitySummary[]>
Comment on lines +9 to +12
}
34 changes: 34 additions & 0 deletions packages/react-native-healthkit/src/types/ActivitySummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* One HKActivitySummary — the per-day rollup behind the Apple activity rings.
*
* Activity summaries are keyed by calendar day, not by timestamp, so the date is
* exposed as plain calendar components from `HKActivitySummary.dateComponents(for:)`
* rather than a `Date` — converting to a timestamp would force a timezone assumption
* onto callers.
*
* @see {@link https://developer.apple.com/documentation/healthkit/hkactivitysummary Apple Docs }
*/
export interface ActivitySummary {
/** Calendar year of the summary day (user's current calendar). */
readonly dateYear: number
/** Calendar month (1–12). */
readonly dateMonth: number
/** Calendar day of month (1–31). */
readonly dateDay: number
/** Move ring — active energy burned (kcal). */
readonly activeEnergyBurned: number
/** Move ring goal (kcal). */
readonly activeEnergyBurnedGoal: number
/** Exercise ring — Apple exercise time (minutes). */
readonly appleExerciseTime: number
/** Exercise ring goal (minutes). */
readonly appleExerciseTimeGoal: number
/** Stand ring — stand hours (count). */
readonly appleStandHours: number
/** Stand ring goal (count). */
readonly appleStandHoursGoal: number
/** Move-time variant (move-minutes mode) — minutes; 0 when the user's move ring tracks energy. */
readonly appleMoveTime: number
/** Move-time goal (minutes); 0 when unused. */
readonly appleMoveTimeGoal: number
}
1 change: 1 addition & 0 deletions packages/react-native-healthkit/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ActivitySummary'
export * from './Auth'
export * from './Background'
export * from './CategoryType'
Expand Down
Loading