diff --git a/CHANGELOG.md b/CHANGELOG.md index 9176bed..53993cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/ios/Podfile b/example/ios/Podfile index d9b937e..53de287 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -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' diff --git a/example/lib/main.dart b/example/lib/main.dart index 5d35482..3af4dc0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -234,6 +234,11 @@ class _ReadView extends StatelessWidget with HealthKitReporterMixin { workoutRouteQuery(); }, child: Text('workoutRouteQuery')), + TextButton( + onPressed: () { + workoutRouteForUUIDQuery(); + }, + child: Text('workoutRouteForUUIDQuery')), TextButton( onPressed: () { querySources(); @@ -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(); diff --git a/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift b/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift index 52068d2..59e7632 100644 --- a/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift +++ b/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift @@ -24,6 +24,7 @@ extension SwiftHealthKitReporterPlugin { case statisticsQuery case heartbeatSeriesQuery case workoutRouteQuery + case workoutRouteForUUIDQuery case queryActivitySummary case sourceQuery case correlationQuery @@ -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) @@ -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], diff --git a/lib/health_kit_reporter.dart b/lib/health_kit_reporter.dart index dad6955..a58c612 100644 --- a/lib/health_kit_reporter.dart +++ b/lib/health_kit_reporter.dart @@ -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> workoutRouteForUUIDQuery( + UUIDPredicate predicate, + ) async { + final arguments = predicate.map; + final result = await _methodChannel.invokeMethod( + 'workoutRouteForUUIDQuery', + arguments, + ); + final List list = jsonDecode(result); + final routes = []; + for (final Map 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]. /// diff --git a/lib/model/predicate.dart b/lib/model/predicate.dart index 8a1b26e..8a47b44 100644 --- a/lib/model/predicate.dart +++ b/lib/model/predicate.dart @@ -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, @@ -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 get map => { + 'uuid': uuid, + }; +}