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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [2.3.2] - 25.02.2025

* Add getting workout routes for workout UUID

## [2.3.1] - 12.12.2024

* Add missing Workout types
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
16 changes: 16 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ class _ReadView extends StatelessWidget with HealthKitReporterMixin {
workoutRouteQuery();
},
child: Text('workoutRouteQuery')),
TextButton(
onPressed: () {
workoutRouteForUUIDQuery();
},
child: Text('workoutRouteForUUIDQuery')),
TextButton(
onPressed: () {
querySources();
Expand Down Expand Up @@ -344,6 +349,17 @@ class _ReadView extends StatelessWidget with HealthKitReporterMixin {
}
}

void workoutRouteForUUIDQuery() async {
try {
final series = await HealthKitReporter.workoutRouteForUUIDQuery(
const UUIDPredicate('D3A3D3A3-4D3A-4A3A-8A3A-3A3A3A3A3A3A'),
);
print('workoutRoutes: ${series.map((e) => e.map).toList()}');
} catch (e) {
print(e);
}
}

void queryCharacteristics() async {
try {
final characteristics = await HealthKitReporter.characteristicsQuery();
Expand Down
110 changes: 110 additions & 0 deletions ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension SwiftHealthKitReporterPlugin {
case statisticsQuery
case heartbeatSeriesQuery
case workoutRouteQuery
case workoutRouteForUUIDQuery
case queryActivitySummary
case sourceQuery
case correlationQuery
Expand Down Expand Up @@ -172,6 +173,16 @@ extension SwiftHealthKitReporterPlugin {
arguments: arguments,
result: result
)
case .workoutRouteForUUIDQuery:
guard let arguments = call.arguments as? [String: String] else {
throwNoArgumentsError(result: result)
return
}
workoutRouteForUUIDQuery(
reporter: reporter,
arguments: arguments,
result: result
)
case .queryActivitySummary:
guard let arguments = call.arguments as? [String: Double] else {
throwNoArgumentsError(result: result)
Expand Down Expand Up @@ -896,6 +907,105 @@ extension SwiftHealthKitReporterPlugin {
)
}
}
private func getWorkoutByID(
reporter: HealthKitReporter,
workoutUUID: UUID
) async -> HKWorkout? {
let workoutPredicate = HKQuery.predicateForObject(with: workoutUUID)

let samples = try! await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<[HKSample], Error>) in
let query = HKSampleQuery(
sampleType: HKObjectType.workoutType(),
predicate: workoutPredicate,
limit: 1,
sortDescriptors: nil
) { (_, results, error) in

if let hasError = error {
continuation.resume(throwing: hasError)
return
}

guard let samples = results else {
fatalError("workout samples unexpectedly nil")
}

continuation.resume(returning: samples)
}
reporter.manager.executeQuery(query)
}

guard let workouts = samples as? [HKWorkout] else {
return nil
}

return workouts.first ?? nil
}
@available(iOS 13.0.0, *)
private func workoutRouteForUUIDQuery(
reporter: HealthKitReporter,
arguments: [String: String],
result: @escaping FlutterResult
) {
guard
let uuidString = arguments["uuid"],
let uuid = UUID(uuidString: uuidString)
else {
throwParsingArgumentsError(result: result, arguments: arguments)
return
}
Task {
guard let workout = await getWorkoutByID(reporter: reporter, workoutUUID: uuid) else {
result(
FlutterError(
code: "workoutRouteForUUIDQuery",
message: "Error getting workout with provided UUID",
details: uuid
)
)
return
}

let predicate = HKQuery.predicateForObjects(from: workout)
do {
let query = try reporter.reader.workoutRouteQuery(
predicate: predicate
) { (routes, error) in
guard error == nil else {
result(
FlutterError(
code: "workoutRouteForUUIDQuery",
message: "Error in workoutRouteQuery",
details: error.debugDescription
)
)
return
}
do {
result(try routes.encoded())
} catch {
result(
FlutterError(
code: "workoutRouteForUUIDQuery",
message: "Error in json encoding of workout routes: \(routes)",
details: error
)
)
}
}
reporter.manager.executeQuery(query)
} catch let error {
result(
FlutterError(
code: className,
message: "Error in workoutRouteQuery initialization",
details: error
)
)
}
}
}
private func queryActivitySummary(
reporter: HealthKitReporter,
arguments: [String: Double],
Expand Down
21 changes: 21 additions & 0 deletions lib/health_kit_reporter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,27 @@ class HealthKitReporter {
return routes;
}

/// Returns [WorkoutRoute] sample for the provided UUID [predicate].
///
/// Available only for iOS 13.0 and higher.
///
static Future<List<WorkoutRoute>> workoutRouteForUUIDQuery(
UUIDPredicate predicate,
) async {
final arguments = predicate.map;
final result = await _methodChannel.invokeMethod(
'workoutRouteForUUIDQuery',
arguments,
);
final List<dynamic> list = jsonDecode(result);
final routes = <WorkoutRoute>[];
for (final Map<String, dynamic> map in list) {
final sample = WorkoutRoute.fromJson(map);
routes.add(sample);
}
return routes;
}

/// Returns [Quantity] samples for the provided [type],
/// the preferred [unit] and the time interval predicate [predicate].
///
Expand Down
26 changes: 26 additions & 0 deletions lib/model/predicate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'package:health_kit_reporter/health_kit_reporter.dart';
/// For native calls the instance will be mapped to [map]
/// and timestamps values will be accepted as arguments.
///
/// See also: [UUIDPredicate]
///
class Predicate {
const Predicate(
this.startDate,
Expand All @@ -25,3 +27,27 @@ class Predicate {
'endTimestamp': endDate.millisecondsSinceEpoch,
};
}

/// A predicate used in [HealthKitReporter.workoutRouteForUUIDQuery]
/// to filter workout routes associated with the given workout UUID.
///
/// [uuid] - the unique identifier of the workout
///
/// For native calls the instance will be mapped to [map]
/// and accepted as `arguments`.
///
/// See also: [Predicate]
///
class UUIDPredicate {
const UUIDPredicate(
this.uuid,
);

final String uuid;

/// General map representation
///
Map<String, String> get map => {
'uuid': uuid,
};
}