diff --git a/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist b/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist
index 4f8d4d245..8c6e56146 100644
--- a/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/packages/carp_background_location/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 11.0
+ 12.0
diff --git a/packages/carp_background_location/example/ios/Podfile b/packages/carp_background_location/example/ios/Podfile
index 88359b225..279576f38 100644
--- a/packages/carp_background_location/example/ios/Podfile
+++ b/packages/carp_background_location/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '11.0'
+# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj b/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj
index 9ce43b052..2ed38acdd 100644
--- a/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/carp_background_location/example/ios/Runner.xcodeproj/project.pbxproj
@@ -163,7 +163,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -350,7 +350,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -436,7 +436,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -485,7 +485,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3db53b6e1..4f746537f 100644
--- a/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/carp_background_location/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
diff --git a/packages/carp_background_location/example/ios/Runner/AppDelegate.swift b/packages/carp_background_location/example/ios/Runner/AppDelegate.swift
index 4030a5522..87d79a23a 100644
--- a/packages/carp_background_location/example/ios/Runner/AppDelegate.swift
+++ b/packages/carp_background_location/example/ios/Runner/AppDelegate.swift
@@ -8,7 +8,7 @@ func registerPlugins(registry: FlutterPluginRegistry) -> () {
}
}
-@UIApplicationMain
+@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md
index 2ded2babd..d9ef81651 100644
--- a/packages/health/CHANGELOG.md
+++ b/packages/health/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 13.0.0
+
+* Refactored Swift native implementation
+
## 12.2.1
* iOS: Add `swift_version` for add-to-app implementations - PR [#1205](https://github.com/cph-cachet/flutter-plugins/pull/1205)
diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj
index f9034579c..b856ee37b 100644
--- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj
@@ -249,7 +249,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
- ABB05D852D6BB16700FA4740 /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
+ ABB05D852D6BB16700FA4740 /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
@@ -761,7 +761,7 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- ABB05D852D6BB16700FA4740 /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
+ ABB05D852D6BB16700FA4740 /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
diff --git a/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index b76580770..23618233e 100644
--- a/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/health/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
dataTypesIOS = [
HealthDataType.ACTIVE_ENERGY_BURNED,
HealthDataType.AUDIOGRAM,
@@ -65,11 +65,7 @@ const List dataTypesIOS = [
HealthDataType.UV_INDEX,
];
-/// List of data types available on Android.
-///
-/// Note that these are only the ones supported on Android's Health Connect API.
-/// Android's Health Connect has more types that we support in the [HealthDataType]
-/// enumeration.
+/// Data types available on Android via the Google Health Connect API.
const List dataTypesAndroid = [
HealthDataType.ACTIVE_ENERGY_BURNED,
HealthDataType.BASAL_ENERGY_BURNED,
@@ -81,7 +77,7 @@ const List dataTypesAndroid = [
HealthDataType.HEIGHT,
HealthDataType.WEIGHT,
HealthDataType.LEAN_BODY_MASS,
- // HealthDataType.BODY_MASS_INDEX,
+ HealthDataType.BODY_MASS_INDEX,
HealthDataType.BODY_TEMPERATURE,
HealthDataType.HEART_RATE,
HealthDataType.HEART_RATE_VARIABILITY_RMSSD,
diff --git a/packages/health/ios/Classes/HealthConstants.swift b/packages/health/ios/Classes/HealthConstants.swift
new file mode 100644
index 000000000..cda0a14b5
--- /dev/null
+++ b/packages/health/ios/Classes/HealthConstants.swift
@@ -0,0 +1,211 @@
+import HealthKit
+
+/// Constants used across the Health plugin
+enum HealthConstants {
+ // Recording methods
+ enum RecordingMethod: Int {
+ case unknown = 0 // RECORDING_METHOD_UNKNOWN (not supported on iOS)
+ case active = 1 // RECORDING_METHOD_ACTIVELY_RECORDED (not supported on iOS)
+ case automatic = 2 // RECORDING_METHOD_AUTOMATICALLY_RECORDED
+ case manual = 3 // RECORDING_METHOD_MANUAL_ENTRY
+ }
+
+ // Health Data Type Keys
+ static let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED"
+ static let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN"
+ static let AUDIOGRAM = "AUDIOGRAM"
+ static let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED"
+ static let BLOOD_GLUCOSE = "BLOOD_GLUCOSE"
+ static let BLOOD_OXYGEN = "BLOOD_OXYGEN"
+ static let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC"
+ static let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC"
+ static let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE"
+ static let LEAN_BODY_MASS = "LEAN_BODY_MASS"
+ static let BODY_MASS_INDEX = "BODY_MASS_INDEX"
+ static let BODY_TEMPERATURE = "BODY_TEMPERATURE"
+
+ // Nutrition
+ static let DIETARY_CARBS_CONSUMED = "DIETARY_CARBS_CONSUMED"
+ static let DIETARY_ENERGY_CONSUMED = "DIETARY_ENERGY_CONSUMED"
+ static let DIETARY_FATS_CONSUMED = "DIETARY_FATS_CONSUMED"
+ static let DIETARY_PROTEIN_CONSUMED = "DIETARY_PROTEIN_CONSUMED"
+ static let DIETARY_CAFFEINE = "DIETARY_CAFFEINE"
+ static let DIETARY_FIBER = "DIETARY_FIBER"
+ static let DIETARY_SUGAR = "DIETARY_SUGAR"
+ static let DIETARY_FAT_MONOUNSATURATED = "DIETARY_FAT_MONOUNSATURATED"
+ static let DIETARY_FAT_POLYUNSATURATED = "DIETARY_FAT_POLYUNSATURATED"
+ static let DIETARY_FAT_SATURATED = "DIETARY_FAT_SATURATED"
+ static let DIETARY_CHOLESTEROL = "DIETARY_CHOLESTEROL"
+ static let DIETARY_VITAMIN_A = "DIETARY_VITAMIN_A"
+ static let DIETARY_THIAMIN = "DIETARY_THIAMIN"
+ static let DIETARY_RIBOFLAVIN = "DIETARY_RIBOFLAVIN"
+ static let DIETARY_NIACIN = "DIETARY_NIACIN"
+ static let DIETARY_PANTOTHENIC_ACID = "DIETARY_PANTOTHENIC_ACID"
+ static let DIETARY_VITAMIN_B6 = "DIETARY_VITAMIN_B6"
+ static let DIETARY_BIOTIN = "DIETARY_BIOTIN"
+ static let DIETARY_VITAMIN_B12 = "DIETARY_VITAMIN_B12"
+ static let DIETARY_VITAMIN_C = "DIETARY_VITAMIN_C"
+ static let DIETARY_VITAMIN_D = "DIETARY_VITAMIN_D"
+ static let DIETARY_VITAMIN_E = "DIETARY_VITAMIN_E"
+ static let DIETARY_VITAMIN_K = "DIETARY_VITAMIN_K"
+ static let DIETARY_FOLATE = "DIETARY_FOLATE"
+ static let DIETARY_CALCIUM = "DIETARY_CALCIUM"
+ static let DIETARY_CHLORIDE = "DIETARY_CHLORIDE"
+ static let DIETARY_IRON = "DIETARY_IRON"
+ static let DIETARY_MAGNESIUM = "DIETARY_MAGNESIUM"
+ static let DIETARY_PHOSPHORUS = "DIETARY_PHOSPHORUS"
+ static let DIETARY_POTASSIUM = "DIETARY_POTASSIUM"
+ static let DIETARY_SODIUM = "DIETARY_SODIUM"
+ static let DIETARY_ZINC = "DIETARY_ZINC"
+ static let DIETARY_WATER = "WATER"
+ static let DIETARY_CHROMIUM = "DIETARY_CHROMIUM"
+ static let DIETARY_COPPER = "DIETARY_COPPER"
+ static let DIETARY_IODINE = "DIETARY_IODINE"
+ static let DIETARY_MANGANESE = "DIETARY_MANGANESE"
+ static let DIETARY_MOLYBDENUM = "DIETARY_MOLYBDENUM"
+ static let DIETARY_SELENIUM = "DIETARY_SELENIUM"
+
+ static let NUTRITION_KEYS: [String: HKQuantityTypeIdentifier] = [
+ "calories": .dietaryEnergyConsumed,
+ "protein": .dietaryProtein,
+ "carbs": .dietaryCarbohydrates,
+ "fat": .dietaryFatTotal,
+ "caffeine": .dietaryCaffeine,
+ "vitamin_a": .dietaryVitaminA,
+ "b1_thiamine": .dietaryThiamin,
+ "b2_riboflavin": .dietaryRiboflavin,
+ "b3_niacin" : .dietaryNiacin,
+ "b5_pantothenic_acid" : .dietaryPantothenicAcid,
+ "b6_pyridoxine" : .dietaryVitaminB6,
+ "b7_biotin" : .dietaryBiotin,
+ "b9_folate" : .dietaryFolate,
+ "b12_cobalamin": .dietaryVitaminB12,
+ "vitamin_c": .dietaryVitaminC,
+ "vitamin_d": .dietaryVitaminD,
+ "vitamin_e": .dietaryVitaminE,
+ "vitamin_k": .dietaryVitaminK,
+ "calcium": .dietaryCalcium,
+ "chloride": .dietaryChloride,
+ "cholesterol": .dietaryCholesterol,
+ "chromium": .dietaryChromium,
+ "copper": .dietaryCopper,
+ "fat_unsaturated": .dietaryFatMonounsaturated,
+ "fat_monounsaturated": .dietaryFatMonounsaturated,
+ "fat_polyunsaturated": .dietaryFatPolyunsaturated,
+ "fat_saturated": .dietaryFatSaturated,
+ // "fat_trans_monoenoic": .dietaryFatTransMonoenoic,
+ "fiber": .dietaryFiber,
+ "iodine": .dietaryIodine,
+ "iron": .dietaryIron,
+ "magnesium": .dietaryMagnesium,
+ "manganese": .dietaryManganese,
+ "molybdenum": .dietaryMolybdenum,
+ "phosphorus": .dietaryPhosphorus,
+ "potassium": .dietaryPotassium,
+ "selenium": .dietarySelenium,
+ "sodium": .dietarySodium,
+ "sugar": .dietarySugar,
+ "water": .dietaryWater,
+ "zinc": .dietaryZinc,
+ ]
+
+ static let ELECTRODERMAL_ACTIVITY = "ELECTRODERMAL_ACTIVITY"
+ static let FORCED_EXPIRATORY_VOLUME = "FORCED_EXPIRATORY_VOLUME"
+ static let HEART_RATE = "HEART_RATE"
+ static let HEART_RATE_VARIABILITY_SDNN = "HEART_RATE_VARIABILITY_SDNN"
+ static let HEIGHT = "HEIGHT"
+ static let INSULIN_DELIVERY = "INSULIN_DELIVERY"
+ static let HIGH_HEART_RATE_EVENT = "HIGH_HEART_RATE_EVENT"
+ static let IRREGULAR_HEART_RATE_EVENT = "IRREGULAR_HEART_RATE_EVENT"
+ static let LOW_HEART_RATE_EVENT = "LOW_HEART_RATE_EVENT"
+ static let RESTING_HEART_RATE = "RESTING_HEART_RATE"
+ static let RESPIRATORY_RATE = "RESPIRATORY_RATE"
+ static let PERIPHERAL_PERFUSION_INDEX = "PERIPHERAL_PERFUSION_INDEX"
+ static let STEPS = "STEPS"
+ static let WAIST_CIRCUMFERENCE = "WAIST_CIRCUMFERENCE"
+ static let WALKING_HEART_RATE = "WALKING_HEART_RATE"
+ static let WEIGHT = "WEIGHT"
+ static let DISTANCE_WALKING_RUNNING = "DISTANCE_WALKING_RUNNING"
+ static let DISTANCE_SWIMMING = "DISTANCE_SWIMMING"
+ static let DISTANCE_CYCLING = "DISTANCE_CYCLING"
+ static let FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED"
+ static let MINDFULNESS = "MINDFULNESS"
+ static let SLEEP_ASLEEP = "SLEEP_ASLEEP"
+ static let SLEEP_AWAKE = "SLEEP_AWAKE"
+ static let SLEEP_DEEP = "SLEEP_DEEP"
+ static let SLEEP_IN_BED = "SLEEP_IN_BED"
+ static let SLEEP_LIGHT = "SLEEP_LIGHT"
+ static let SLEEP_REM = "SLEEP_REM"
+
+ static let EXERCISE_TIME = "EXERCISE_TIME"
+ static let WORKOUT = "WORKOUT"
+ static let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED"
+ static let HEADACHE_NOT_PRESENT = "HEADACHE_NOT_PRESENT"
+ static let HEADACHE_MILD = "HEADACHE_MILD"
+ static let HEADACHE_MODERATE = "HEADACHE_MODERATE"
+ static let HEADACHE_SEVERE = "HEADACHE_SEVERE"
+ static let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM"
+ static let NUTRITION = "NUTRITION"
+ static let BIRTH_DATE = "BIRTH_DATE"
+ static let GENDER = "GENDER"
+ static let BLOOD_TYPE = "BLOOD_TYPE"
+ static let MENSTRUATION_FLOW = "MENSTRUATION_FLOW"
+ static let WATER_TEMPERATURE = "WATER_TEMPERATURE"
+ static let UNDERWATER_DEPTH = "UNDERWATER_DEPTH"
+ static let UV_INDEX = "UV_INDEX"
+
+ // Health Unit types
+ static let GRAM = "GRAM"
+ static let KILOGRAM = "KILOGRAM"
+ static let OUNCE = "OUNCE"
+ static let POUND = "POUND"
+ static let STONE = "STONE"
+ static let METER = "METER"
+ static let INCH = "INCH"
+ static let FOOT = "FOOT"
+ static let YARD = "YARD"
+ static let MILE = "MILE"
+ static let LITER = "LITER"
+ static let MILLILITER = "MILLILITER"
+ static let FLUID_OUNCE_US = "FLUID_OUNCE_US"
+ static let FLUID_OUNCE_IMPERIAL = "FLUID_OUNCE_IMPERIAL"
+ static let CUP_US = "CUP_US"
+ static let CUP_IMPERIAL = "CUP_IMPERIAL"
+ static let PINT_US = "PINT_US"
+ static let PINT_IMPERIAL = "PINT_IMPERIAL"
+ static let PASCAL = "PASCAL"
+ static let MILLIMETER_OF_MERCURY = "MILLIMETER_OF_MERCURY"
+ static let INCHES_OF_MERCURY = "INCHES_OF_MERCURY"
+ static let CENTIMETER_OF_WATER = "CENTIMETER_OF_WATER"
+ static let ATMOSPHERE = "ATMOSPHERE"
+ static let DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL = "DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL"
+ static let SECOND = "SECOND"
+ static let MILLISECOND = "MILLISECOND"
+ static let MINUTE = "MINUTE"
+ static let HOUR = "HOUR"
+ static let DAY = "DAY"
+ static let JOULE = "JOULE"
+ static let KILOCALORIE = "KILOCALORIE"
+ static let LARGE_CALORIE = "LARGE_CALORIE"
+ static let SMALL_CALORIE = "SMALL_CALORIE"
+ static let DEGREE_CELSIUS = "DEGREE_CELSIUS"
+ static let DEGREE_FAHRENHEIT = "DEGREE_FAHRENHEIT"
+ static let KELVIN = "KELVIN"
+ static let DECIBEL_HEARING_LEVEL = "DECIBEL_HEARING_LEVEL"
+ static let HERTZ = "HERTZ"
+ static let SIEMEN = "SIEMEN"
+ static let VOLT = "VOLT"
+ static let INTERNATIONAL_UNIT = "INTERNATIONAL_UNIT"
+ static let COUNT = "COUNT"
+ static let PERCENT = "PERCENT"
+ static let BEATS_PER_MINUTE = "BEATS_PER_MINUTE"
+ static let RESPIRATIONS_PER_MINUTE = "RESPIRATIONS_PER_MINUTE"
+ static let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER"
+ static let UNKNOWN_UNIT = "UNKNOWN_UNIT"
+ static let NO_UNIT = "NO_UNIT"
+}
+
+/// Error structure used throughout the plugin
+struct PluginError: Error {
+ let message: String
+}
diff --git a/packages/health/ios/Classes/HealthDataOperations.swift b/packages/health/ios/Classes/HealthDataOperations.swift
new file mode 100644
index 000000000..664e82afe
--- /dev/null
+++ b/packages/health/ios/Classes/HealthDataOperations.swift
@@ -0,0 +1,299 @@
+import HealthKit
+import Flutter
+
+/// Class for managing health data permissions and deletion operations
+class HealthDataOperations {
+ let healthStore: HKHealthStore
+ let dataTypesDict: [String: HKSampleType]
+ let characteristicsTypesDict: [String: HKCharacteristicType]
+ let nutritionList: [String]
+
+ /// - Parameters:
+ /// - healthStore: The HealthKit store
+ /// - dataTypesDict: Dictionary of data types
+ /// - characteristicsTypesDict: Dictionary of characteristic types
+ /// - nutritionList: List of nutrition data types
+ init(healthStore: HKHealthStore,
+ dataTypesDict: [String: HKSampleType],
+ characteristicsTypesDict: [String: HKCharacteristicType],
+ nutritionList: [String]) {
+ self.healthStore = healthStore
+ self.dataTypesDict = dataTypesDict
+ self.characteristicsTypesDict = characteristicsTypesDict
+ self.nutritionList = nutritionList
+ }
+
+ /// Check if HealthKit is available on the device
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) {
+ result(HKHealthStore.isHealthDataAvailable())
+ }
+
+ /// Check if we have required permissions
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ let arguments = call.arguments as? NSDictionary
+ guard var types = arguments?["types"] as? [String],
+ var permissions = arguments?["permissions"] as? [Int],
+ types.count == permissions.count
+ else {
+ throw PluginError(message: "Invalid Arguments!")
+ }
+
+ if let nutritionIndex = types.firstIndex(of: HealthConstants.NUTRITION) {
+ types.remove(at: nutritionIndex)
+ let nutritionPermission = permissions[nutritionIndex]
+ permissions.remove(at: nutritionIndex)
+
+ for nutritionType in nutritionList {
+ types.append(nutritionType)
+ permissions.append(nutritionPermission)
+ }
+ }
+
+ for (index, type) in types.enumerated() {
+ guard let sampleType = dataTypesDict[type] else {
+ print("Warning: Health data type '\(type)' not found in dataTypesDict")
+ result(false)
+ return
+ }
+
+ let success = hasPermission(type: sampleType, access: permissions[index])
+ if success == nil || success == false {
+ result(success)
+ return
+ }
+ if let characteristicType = characteristicsTypesDict[type] {
+ let characteristicSuccess = hasPermission(type: characteristicType, access: permissions[index])
+ if (characteristicSuccess == nil || characteristicSuccess == false) {
+ result(characteristicSuccess)
+ return
+ }
+ }
+ }
+
+ result(true)
+ }
+
+ /// Check if we have permission for a specific type
+ /// - Parameters:
+ /// - type: The object type to check
+ /// - access: Access level (0: read, 1: write, other: read/write)
+ /// - Returns: Bool or nil depending on permission status
+ private func hasPermission(type: HKObjectType, access: Int) -> Bool? {
+ if #available(iOS 13.0, *) {
+ let status = healthStore.authorizationStatus(for: type)
+ switch access {
+ case 0: // READ
+ return nil
+ case 1: // WRITE
+ return (status == HKAuthorizationStatus.sharingAuthorized)
+ default: // READ_WRITE
+ return nil
+ }
+ } else {
+ return nil
+ }
+ }
+
+ /// Request authorization for health data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let types = arguments["types"] as? [String],
+ let permissions = arguments["permissions"] as? [Int],
+ permissions.count == types.count
+ else {
+ throw PluginError(message: "Invalid Arguments!")
+ }
+
+ var typesToRead = Set()
+ var typesToWrite = Set()
+
+ for (index, key) in types.enumerated() {
+ if (key == HealthConstants.NUTRITION) {
+ for nutritionType in nutritionList {
+ guard let nutritionData = dataTypesDict[nutritionType] else {
+ print("Warning: Nutrition data type '\(nutritionType)' not found in dataTypesDict")
+ continue
+ }
+ typesToWrite.insert(nutritionData)
+ }
+ } else {
+ let access = permissions[index]
+
+ if let dataType = dataTypesDict[key] {
+ switch access {
+ case 0:
+ typesToRead.insert(dataType)
+ case 1:
+ typesToWrite.insert(dataType)
+ default:
+ typesToRead.insert(dataType)
+ typesToWrite.insert(dataType)
+ }
+ }
+
+ if let characteristicsType = characteristicsTypesDict[key] {
+ switch access {
+ case 0:
+ typesToRead.insert(characteristicsType)
+ case 1:
+ throw PluginError(message: "Cannot request write permission for characteristic type \(characteristicsType)")
+ default:
+ typesToRead.insert(characteristicsType)
+ }
+ }
+
+ if dataTypesDict[key] == nil && characteristicsTypesDict[key] == nil {
+ print("Warning: Health data type '\(key)' not found in dataTypesDict or characteristicsTypesDict")
+ }
+
+ }
+ }
+
+ if #available(iOS 13.0, *) {
+ healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) {
+ (success, error) in
+ DispatchQueue.main.async {
+ result(success)
+ }
+ }
+ } else {
+ // TODO: Add proper error handling
+ result(false)
+ }
+ }
+
+ /// Delete health data by date range
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func delete(call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let arguments = call.arguments as? NSDictionary,
+ let dataTypeKey = arguments["dataTypeKey"] as? String else {
+ print("Error: Missing dataTypeKey in arguments")
+ result(false)
+ return
+ }
+
+ // Check if it's a characteristic type - these cannot be deleted
+ if characteristicsTypesDict[dataTypeKey] != nil {
+ print("Info: Cannot delete characteristic type '\(dataTypeKey)' - these are read-only system values")
+ result(false)
+ return
+ }
+
+ let startTime = (arguments["startTime"] as? NSNumber) ?? 0
+ let endTime = (arguments["endTime"] as? NSNumber) ?? 0
+
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ guard let dataType = dataTypesDict[dataTypeKey] else {
+ print("Warning: Health data type '\(dataTypeKey)' not found in dataTypesDict")
+ result(false)
+ return
+ }
+
+ let samplePredicate = HKQuery.predicateForSamples(
+ withStart: dateFrom, end: dateTo, options: .strictStartDate)
+ let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default())
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
+
+ let deleteQuery = HKSampleQuery(
+ sampleType: dataType,
+ predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]),
+ limit: HKObjectQueryNoLimit,
+ sortDescriptors: [sortDescriptor]
+ ) { [weak self] x, samplesOrNil, error in
+ guard let self = self else { return }
+
+ guard let samplesOrNil = samplesOrNil, error == nil else {
+ print("Error querying \(dataType) samples: \(error?.localizedDescription ?? "Unknown error")")
+ DispatchQueue.main.async {
+ result(false)
+ }
+ return
+ }
+
+ // Chcek if there are any samples to delete
+ if samplesOrNil.isEmpty {
+ print("Info: No \(dataType) samples found in the specified date range.")
+ DispatchQueue.main.async {
+ result(true)
+ }
+ return
+ }
+
+ // Delete the retrieved objects from the HealthKit store
+ self.healthStore.delete(samplesOrNil) { (success, error) in
+ if let err = error {
+ print("Error deleting \(dataType) Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ }
+ }
+
+ healthStore.execute(deleteQuery)
+ }
+
+ /// Delete health data by UUID
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func deleteByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let uuidarg = arguments["uuid"] as? String,
+ let dataTypeKey = arguments["dataTypeKey"] as? String else {
+ throw PluginError(message: "Invalid Arguments - UUID or DataTypeKey invalid")
+ }
+
+ guard let dataTypeToRemove = dataTypesDict[dataTypeKey] else {
+ print("Warning: Health data type '\(dataTypeKey)' not found in dataTypesDict")
+ result(false)
+ return
+ }
+
+ guard let uuid = UUID(uuidString: uuidarg) else {
+ result(false)
+ return
+ }
+ let predicate = HKQuery.predicateForObjects(with: [uuid])
+
+ let query = HKSampleQuery(
+ sampleType: dataTypeToRemove,
+ predicate: predicate,
+ limit: 1,
+ sortDescriptors: nil
+ ) { [weak self] query, samplesOrNil, error in
+ guard let self = self else { return }
+
+ guard let samples = samplesOrNil, !samples.isEmpty else {
+ DispatchQueue.main.async {
+ result(false)
+ }
+ return
+ }
+
+ self.healthStore.delete(samples) { success, error in
+ if let error = error {
+ print("Error deleting sample with UUID \(uuid): \(error.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ }
+ }
+
+ healthStore.execute(query)
+ }
+}
diff --git a/packages/health/ios/Classes/HealthDataReader.swift b/packages/health/ios/Classes/HealthDataReader.swift
new file mode 100644
index 000000000..baf380f9e
--- /dev/null
+++ b/packages/health/ios/Classes/HealthDataReader.swift
@@ -0,0 +1,550 @@
+import HealthKit
+import Flutter
+
+/// Class responsible for reading health data from HealthKit
+class HealthDataReader {
+ let healthStore: HKHealthStore
+ let dataTypesDict: [String: HKSampleType]
+ let dataQuantityTypesDict: [String: HKQuantityType]
+ let unitDict: [String: HKUnit]
+ let workoutActivityTypeMap: [String: HKWorkoutActivityType]
+ let characteristicsTypesDict: [String: HKCharacteristicType]
+
+ /// - Parameters:
+ /// - healthStore: The HealthKit store
+ /// - dataTypesDict: Dictionary of data types
+ /// - dataQuantityTypesDict: Dictionary of quantity types
+ /// - unitDict: Dictionary of units
+ /// - workoutActivityTypeMap: Dictionary of workout activity types
+ /// - characteristicsTypesDict: Dictionary of characteristic types
+ init(healthStore: HKHealthStore,
+ dataTypesDict: [String: HKSampleType],
+ dataQuantityTypesDict: [String: HKQuantityType],
+ unitDict: [String: HKUnit],
+ workoutActivityTypeMap: [String: HKWorkoutActivityType],
+ characteristicsTypesDict: [String: HKCharacteristicType]) {
+ self.healthStore = healthStore
+ self.dataTypesDict = dataTypesDict
+ self.dataQuantityTypesDict = dataQuantityTypesDict
+ self.unitDict = unitDict
+ self.workoutActivityTypeMap = workoutActivityTypeMap
+ self.characteristicsTypesDict = characteristicsTypesDict
+ }
+
+ /// Gets health data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func getData(call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let arguments = call.arguments as? NSDictionary,
+ let dataTypeKey = arguments["dataTypeKey"] as? String else {
+ DispatchQueue.main.async {
+ result(FlutterError(code: "ARGUMENT_ERROR",
+ message: "Missing required dataTypeKey argument",
+ details: nil))
+ }
+ return
+ }
+
+ let dataUnitKey = arguments["dataUnitKey"] as? String
+ let startTime = (arguments["startTime"] as? NSNumber) ?? 0
+ let endTime = (arguments["endTime"] as? NSNumber) ?? 0
+ let limit = (arguments["limit"] as? Int) ?? HKObjectQueryNoLimit
+ let recordingMethodsToFilter = (arguments["recordingMethodsToFilter"] as? [Int]) ?? []
+ let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue)
+
+ // convert from milliseconds to Date()
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let sourceIdForCharacteristic = "com.apple.Health"
+ let sourceNameForCharacteristic = "Health"
+
+ // characteristic types checks (like GENDER, BLOOD_TYPE, etc.)
+ switch(dataTypeKey) {
+ case HealthConstants.BIRTH_DATE:
+ let dateOfBirth = getBirthDate()
+ result([
+ [
+ "value": dateOfBirth?.timeIntervalSince1970,
+ "date_from": Int(dateFrom.timeIntervalSince1970 * 1000),
+ "date_to": Int(dateTo.timeIntervalSince1970 * 1000),
+ "source_id": sourceIdForCharacteristic,
+ "source_name": sourceNameForCharacteristic,
+ "recording_method": HealthConstants.RecordingMethod.manual.rawValue
+ ]
+ ])
+ return
+ case HealthConstants.GENDER:
+ let gender = getGender()
+ result([
+ [
+ "value": gender?.rawValue,
+ "date_from": Int(dateFrom.timeIntervalSince1970 * 1000),
+ "date_to": Int(dateTo.timeIntervalSince1970 * 1000),
+ "source_id": sourceIdForCharacteristic,
+ "source_name": sourceNameForCharacteristic,
+ "recording_method": HealthConstants.RecordingMethod.manual.rawValue
+ ]
+ ])
+ return
+ case HealthConstants.BLOOD_TYPE:
+ let bloodType = getBloodType()
+ result([
+ [
+ "value": bloodType?.rawValue,
+ "date_from": Int(dateFrom.timeIntervalSince1970 * 1000),
+ "date_to": Int(dateTo.timeIntervalSince1970 * 1000),
+ "source_id": sourceIdForCharacteristic,
+ "source_name": sourceNameForCharacteristic,
+ "recording_method": HealthConstants.RecordingMethod.manual.rawValue
+ ]
+ ])
+ return
+ default:
+ break
+ }
+
+ guard let dataType = dataTypesDict[dataTypeKey] else {
+ DispatchQueue.main.async {
+ result(FlutterError(code: "INVALID_TYPE",
+ message: "Invalid dataTypeKey: \(dataTypeKey)",
+ details: nil))
+ }
+ return
+ }
+
+ var unit: HKUnit?
+ if let dataUnitKey = dataUnitKey {
+ unit = unitDict[dataUnitKey]
+ }
+
+ var predicate = HKQuery.predicateForSamples(
+ withStart: dateFrom, end: dateTo, options: .strictStartDate)
+ if (!includeManualEntry) {
+ let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
+ predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
+ }
+ let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
+
+ let query = HKSampleQuery(
+ sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]
+ ) { x, samplesOrNil, error in
+
+ guard error == nil else {
+ DispatchQueue.main.async {
+ result(FlutterError(code: "HEALTH_ERROR",
+ message: "Error getting health data: \(error!.localizedDescription)",
+ details: nil))
+ }
+ return
+ }
+
+ guard let samples = samplesOrNil else {
+ DispatchQueue.main.async {
+ result([])
+ }
+ return
+ }
+
+ if let quantitySamples = samples as? [HKQuantitySample] {
+ let dictionaries = quantitySamples.map { sample -> NSDictionary in
+ return [
+ "uuid": "\(sample.uuid)",
+ "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()),
+ "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
+ "source_id": sample.sourceRevision.source.bundleIdentifier,
+ "source_name": sample.sourceRevision.source.name,
+ "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
+ ? HealthConstants.RecordingMethod.manual.rawValue
+ : HealthConstants.RecordingMethod.automatic.rawValue,
+ "dataUnitKey": unit?.unitString,
+ "metadata": HealthUtilities.sanitizeMetadata(sample.metadata)
+ ]
+ }
+ DispatchQueue.main.async {
+ result(dictionaries)
+ }
+ } else if var categorySamples = samples as? [HKCategorySample] {
+ // filter category samples based on dataTypeKey
+ switch dataTypeKey {
+ case HealthConstants.SLEEP_IN_BED:
+ categorySamples = categorySamples.filter { $0.value == 0 }
+ case HealthConstants.SLEEP_ASLEEP:
+ categorySamples = categorySamples.filter { $0.value == 1 }
+ case HealthConstants.SLEEP_AWAKE:
+ categorySamples = categorySamples.filter { $0.value == 2 }
+ case HealthConstants.SLEEP_LIGHT:
+ categorySamples = categorySamples.filter { $0.value == 3 }
+ case HealthConstants.SLEEP_DEEP:
+ categorySamples = categorySamples.filter { $0.value == 4 }
+ case HealthConstants.SLEEP_REM:
+ categorySamples = categorySamples.filter { $0.value == 5 }
+ case HealthConstants.HEADACHE_UNSPECIFIED:
+ categorySamples = categorySamples.filter { $0.value == 0 }
+ case HealthConstants.HEADACHE_NOT_PRESENT:
+ categorySamples = categorySamples.filter { $0.value == 1 }
+ case HealthConstants.HEADACHE_MILD:
+ categorySamples = categorySamples.filter { $0.value == 2 }
+ case HealthConstants.HEADACHE_MODERATE:
+ categorySamples = categorySamples.filter { $0.value == 3 }
+ case HealthConstants.HEADACHE_SEVERE:
+ categorySamples = categorySamples.filter { $0.value == 4 }
+ default:
+ break
+ }
+
+ let categories = categorySamples.map { sample -> NSDictionary in
+ return [
+ "uuid": "\(sample.uuid)",
+ "value": sample.value,
+ "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
+ "source_id": sample.sourceRevision.source.bundleIdentifier,
+ "source_name": sample.sourceRevision.source.name,
+ "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
+ ? HealthConstants.RecordingMethod.manual.rawValue
+ : HealthConstants.RecordingMethod.automatic.rawValue,
+ "metadata": HealthUtilities.sanitizeMetadata(sample.metadata)
+ ]
+ }
+ DispatchQueue.main.async {
+ result(categories)
+ }
+ } else if let workoutSamples = samples as? [HKWorkout] {
+ let dictionaries = workoutSamples.map { sample -> NSDictionary in
+ return [
+ "uuid": "\(sample.uuid)",
+ "workoutActivityType": self.workoutActivityTypeMap.first(where: {
+ $0.value == sample.workoutActivityType
+ })?.key,
+ "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()),
+ "totalEnergyBurnedUnit": "KILOCALORIE",
+ "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()),
+ "totalDistanceUnit": "METER",
+ "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
+ "source_id": sample.sourceRevision.source.bundleIdentifier,
+ "source_name": sample.sourceRevision.source.name,
+ "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
+ ? HealthConstants.RecordingMethod.manual.rawValue
+ : HealthConstants.RecordingMethod.automatic.rawValue,
+ "workout_type": HKWorkoutActivityType.toString(sample.workoutActivityType),
+ "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0,
+ "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0
+ ]
+ }
+
+ DispatchQueue.main.async {
+ result(dictionaries)
+ }
+ } else if let audiogramSamples = samples as? [HKAudiogramSample] {
+ let dictionaries = audiogramSamples.map { sample -> NSDictionary in
+ var frequencies = [Double]()
+ var leftEarSensitivities = [Double]()
+ var rightEarSensitivities = [Double]()
+ for samplePoint in sample.sensitivityPoints {
+ frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz()))
+ leftEarSensitivities.append(
+ samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel()))
+ rightEarSensitivities.append(
+ samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel()))
+ }
+ return [
+ "uuid": "\(sample.uuid)",
+ "frequencies": frequencies,
+ "leftEarSensitivities": leftEarSensitivities,
+ "rightEarSensitivities": rightEarSensitivities,
+ "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
+ "source_id": sample.sourceRevision.source.bundleIdentifier,
+ "source_name": sample.sourceRevision.source.name,
+ ]
+ }
+ DispatchQueue.main.async {
+ result(dictionaries)
+ }
+ } else if let nutritionSamples = samples as? [HKCorrelation] {
+ var foods: [[String: Any?]] = []
+ for food in nutritionSamples {
+ let name = food.metadata?[HKMetadataKeyFoodType] as? String
+ let mealType = food.metadata?["HKFoodMeal"]
+ let samples = food.objects
+ if let sample = samples.first as? HKQuantitySample {
+ var sampleDict = [
+ "uuid": "\(sample.uuid)",
+ "name": name,
+ "meal_type": mealType,
+ "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
+ "source_id": sample.sourceRevision.source.bundleIdentifier,
+ "source_name": sample.sourceRevision.source.name,
+ "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
+ ? HealthConstants.RecordingMethod.manual.rawValue
+ : HealthConstants.RecordingMethod.automatic.rawValue
+ ]
+ for sample in samples {
+ if let quantitySample = sample as? HKQuantitySample {
+ for (key, identifier) in HealthConstants.NUTRITION_KEYS {
+ if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){
+ let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram()
+ sampleDict[key] = quantitySample.quantity.doubleValue(for: unit)
+ }
+ }
+ }
+ }
+ foods.append(sampleDict as! [String : Any?])
+ }
+ }
+
+ DispatchQueue.main.async {
+ result(foods)
+ }
+ } else {
+ if #available(iOS 14.0, *), let ecgSamples = samples as? [HKElectrocardiogram] {
+ let dictionaries = ecgSamples.map(self.fetchEcgMeasurements)
+ DispatchQueue.main.async {
+ result(dictionaries)
+ }
+ } else {
+ DispatchQueue.main.async {
+ print("Error getting ECG - only available on iOS 14.0 and above!")
+ result(nil)
+ }
+ }
+ }
+ }
+
+ healthStore.execute(query)
+ }
+
+ /// Gets interval health data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func getIntervalData(call: FlutterMethodCall, result: @escaping FlutterResult) {
+ let arguments = call.arguments as? NSDictionary
+ let dataTypeKey = (arguments?["dataTypeKey"] as? String) ?? "DEFAULT"
+ let dataUnitKey = (arguments?["dataUnitKey"] as? String)
+ let startDate = (arguments?["startTime"] as? NSNumber) ?? 0
+ let endDate = (arguments?["endTime"] as? NSNumber) ?? 0
+ let intervalInSecond = (arguments?["interval"] as? Int) ?? 1
+ let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? []
+ let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue)
+
+ // interval in seconds
+ var interval = DateComponents()
+ interval.second = intervalInSecond
+
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startDate.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endDate.doubleValue)
+
+ guard let quantityType = dataQuantityTypesDict[dataTypeKey] else {
+ DispatchQueue.main.async {
+ result(FlutterError(code: "INVALID_TYPE",
+ message: "Invalid dataTypeKey for interval query: \(dataTypeKey)",
+ details: nil))
+ }
+ return
+ }
+
+ var predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: [])
+ if (!includeManualEntry) {
+ let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
+ predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
+ }
+
+ let query = HKStatisticsCollectionQuery(
+ quantityType: quantityType,
+ quantitySamplePredicate: predicate,
+ options: [.cumulativeSum, .separateBySource],
+ anchorDate: dateFrom,
+ intervalComponents: interval
+ )
+
+ query.initialResultsHandler = { [weak self] _, statisticCollectionOrNil, error in
+ guard let self = self else {
+ DispatchQueue.main.async {
+ result(FlutterError(code: "INTERNAL_ERROR",
+ message: "Internal instance reference lost",
+ details: nil))
+ }
+ return
+ }
+
+ if let error = error {
+ DispatchQueue.main.async {
+ result(FlutterError(code: "STATISTICS_ERROR",
+ message: "Error getting statistics: \(error.localizedDescription)",
+ details: nil))
+ }
+ return
+ }
+
+ guard let collection = statisticCollectionOrNil else {
+ DispatchQueue.main.async {
+ result(nil)
+ }
+ return
+ }
+
+ var dictionaries = [[String: Any]]()
+ collection.enumerateStatistics(from: dateFrom, to: dateTo) { [weak self] statisticData, _ in
+ guard let self = self else { return }
+
+ if let quantity = statisticData.sumQuantity(),
+ let dataUnitKey = dataUnitKey,
+ let unit = self.unitDict[dataUnitKey] {
+ let dict = [
+ "value": quantity.doubleValue(for: unit),
+ "date_from": Int(statisticData.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(statisticData.endDate.timeIntervalSince1970 * 1000),
+ "source_id": statisticData.sources?.first?.bundleIdentifier ?? "",
+ "source_name": statisticData.sources?.first?.name ?? ""
+ ]
+ dictionaries.append(dict)
+ }
+ }
+ DispatchQueue.main.async {
+ result(dictionaries)
+ }
+ }
+ healthStore.execute(query)
+ }
+
+ /// Gets total steps in interval
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) {
+ let arguments = call.arguments as? NSDictionary
+ let startTime = (arguments?["startTime"] as? NSNumber) ?? 0
+ let endTime = (arguments?["endTime"] as? NSNumber) ?? 0
+ let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? []
+ let includeManualEntry = !recordingMethodsToFilter.contains(HealthConstants.RecordingMethod.manual.rawValue)
+
+ // Convert dates from milliseconds to Date()
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
+ var predicate = HKQuery.predicateForSamples(
+ withStart: dateFrom, end: dateTo, options: .strictStartDate)
+ if (!includeManualEntry) {
+ let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
+ predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
+ }
+
+ let query = HKStatisticsCollectionQuery(
+ quantityType: sampleType,
+ quantitySamplePredicate: predicate,
+ options: .cumulativeSum,
+ anchorDate: dateFrom,
+ intervalComponents: DateComponents(day: 1)
+ )
+ query.initialResultsHandler = { query, results, error in
+ guard let results = results else {
+ let errorMessage = error?.localizedDescription ?? "Unknown error"
+ DispatchQueue.main.async {
+ result(FlutterError(code: "STEPS_ERROR",
+ message: "Error getting step count: \(errorMessage)",
+ details: nil))
+ }
+ return
+ }
+
+ var totalSteps = 0.0
+ results.enumerateStatistics(from: dateFrom, to: dateTo) { statistics, stop in
+ if let quantity = statistics.sumQuantity() {
+ let unit = HKUnit.count()
+ totalSteps += quantity.doubleValue(for: unit)
+ }
+ }
+
+ DispatchQueue.main.async {
+ result(Int(totalSteps))
+ }
+ }
+
+ healthStore.execute(query)
+ }
+
+ /// Gets birth date from HealthKit
+ /// - Returns: Birth date
+ private func getBirthDate() -> Date? {
+ var dob: Date?
+ do {
+ dob = try healthStore.dateOfBirthComponents().date
+ } catch {
+ dob = nil
+ print("Error retrieving date of birth: \(error)")
+ }
+ return dob
+ }
+
+ /// Gets gender from HealthKit
+ /// - Returns: Biological sex
+ private func getGender() -> HKBiologicalSex? {
+ var bioSex: HKBiologicalSex?
+ do {
+ bioSex = try healthStore.biologicalSex().biologicalSex
+ } catch {
+ bioSex = nil
+ print("Error retrieving biologicalSex: \(error)")
+ }
+ return bioSex
+ }
+
+ /// Gets blood type from HealthKit
+ /// - Returns: Blood type
+ private func getBloodType() -> HKBloodType? {
+ var bloodType: HKBloodType?
+ do {
+ bloodType = try healthStore.bloodType().bloodType
+ } catch {
+ bloodType = nil
+ print("Error retrieving blood type: \(error)")
+ }
+ return bloodType
+ }
+
+ /// Fetch ECG measurements from an HKElectrocardiogram sample
+ /// - Parameter sample: ECG sample
+ /// - Returns: Dictionary with ECG data
+ @available(iOS 14.0, *)
+ private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary {
+ let semaphore = DispatchSemaphore(value: 0)
+ var voltageValues = [NSDictionary]()
+ let voltageQuery = HKElectrocardiogramQuery(sample) { query, result in
+ switch result {
+ case let .measurement(measurement):
+ if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) {
+ let voltage = voltageQuantity.doubleValue(for: HKUnit.volt())
+ let timeSinceSampleStart = measurement.timeSinceSampleStart
+ voltageValues.append(["voltage": voltage, "timeSinceSampleStart": timeSinceSampleStart])
+ }
+ case .done:
+ semaphore.signal()
+ case let .error(error):
+ print(error)
+ @unknown default:
+ print("Unknown error occurred")
+ }
+ }
+ healthStore.execute(voltageQuery)
+ semaphore.wait()
+ return [
+ "uuid": "\(sample.uuid)",
+ "voltageValues": voltageValues,
+ "averageHeartRate": sample.averageHeartRate?.doubleValue(
+ for: HKUnit.count().unitDivided(by: HKUnit.minute())),
+ "samplingFrequency": sample.samplingFrequency?.doubleValue(for: HKUnit.hertz()),
+ "classification": sample.classification.rawValue,
+ "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
+ "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
+ "source_id": sample.sourceRevision.source.bundleIdentifier,
+ "source_name": sample.sourceRevision.source.name,
+ ]
+ }
+}
diff --git a/packages/health/ios/Classes/HealthDataWriter.swift b/packages/health/ios/Classes/HealthDataWriter.swift
new file mode 100644
index 000000000..1ea3bf301
--- /dev/null
+++ b/packages/health/ios/Classes/HealthDataWriter.swift
@@ -0,0 +1,360 @@
+import HealthKit
+import Flutter
+
+/// Class responsible for writing health data to HealthKit
+class HealthDataWriter {
+ let healthStore: HKHealthStore
+ let dataTypesDict: [String: HKSampleType]
+ let unitDict: [String: HKUnit]
+ let workoutActivityTypeMap: [String: HKWorkoutActivityType]
+
+ /// - Parameters:
+ /// - healthStore: The HealthKit store
+ /// - dataTypesDict: Dictionary of data types
+ /// - unitDict: Dictionary of units
+ /// - workoutActivityTypeMap: Dictionary of workout activity types
+ init(healthStore: HKHealthStore, dataTypesDict: [String: HKSampleType], unitDict: [String: HKUnit], workoutActivityTypeMap: [String: HKWorkoutActivityType]) {
+ self.healthStore = healthStore
+ self.dataTypesDict = dataTypesDict
+ self.unitDict = unitDict
+ self.workoutActivityTypeMap = workoutActivityTypeMap
+ }
+
+ /// Writes general health data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let value = (arguments["value"] as? Double),
+ let type = (arguments["dataTypeKey"] as? String),
+ let unit = (arguments["dataUnitKey"] as? String),
+ let startTime = (arguments["startTime"] as? NSNumber),
+ let endTime = (arguments["endTime"] as? NSNumber),
+ let recordingMethod = (arguments["recordingMethod"] as? Int)
+ else {
+ throw PluginError(message: "Invalid Arguments")
+ }
+
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let isManualEntry = recordingMethod == HealthConstants.RecordingMethod.manual.rawValue
+ let metadata: [String: Any] = [
+ HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)
+ ]
+
+ let sample: HKObject
+
+ if dataTypesDict[type]!.isKind(of: HKCategoryType.self) {
+ sample = HKCategorySample(
+ type: dataTypesDict[type] as! HKCategoryType, value: Int(value), start: dateFrom,
+ end: dateTo, metadata: metadata)
+ } else {
+ let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value)
+ sample = HKQuantitySample(
+ type: dataTypesDict[type] as! HKQuantityType, quantity: quantity, start: dateFrom,
+ end: dateTo, metadata: metadata)
+ }
+
+ healthStore.save(
+ sample,
+ withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving \(type) Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ }
+
+ /// Writes audiogram data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let frequencies = (arguments["frequencies"] as? [Double]),
+ let leftEarSensitivities = (arguments["leftEarSensitivities"] as? [Double]),
+ let rightEarSensitivities = (arguments["rightEarSensitivities"] as? [Double]),
+ let startTime = (arguments["startTime"] as? NSNumber),
+ let endTime = (arguments["endTime"] as? NSNumber)
+ else {
+ throw PluginError(message: "Invalid Arguments")
+ }
+
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ var sensitivityPoints = [HKAudiogramSensitivityPoint]()
+
+ for index in 0...frequencies.count - 1 {
+ let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index])
+ let dbUnit = HKUnit.decibelHearingLevel()
+ let left = HKQuantity(unit: dbUnit, doubleValue: leftEarSensitivities[index])
+ let right = HKQuantity(unit: dbUnit, doubleValue: rightEarSensitivities[index])
+ let sensitivityPoint = try HKAudiogramSensitivityPoint(
+ frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right)
+ sensitivityPoints.append(sensitivityPoint)
+ }
+
+ let audiogram: HKAudiogramSample
+ let metadataReceived = (arguments["metadata"] as? [String: Any]?)
+
+ if (metadataReceived) != nil {
+ guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return }
+ guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return }
+
+ audiogram = HKAudiogramSample(
+ sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo,
+ metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID])
+
+ } else {
+ audiogram = HKAudiogramSample(
+ sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil)
+ }
+
+ healthStore.save(
+ audiogram,
+ withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving Audiogram. Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ }
+
+ /// Writes blood pressure data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let systolic = (arguments["systolic"] as? Double),
+ let diastolic = (arguments["diastolic"] as? Double),
+ let startTime = (arguments["startTime"] as? NSNumber),
+ let endTime = (arguments["endTime"] as? NSNumber),
+ let recordingMethod = (arguments["recordingMethod"] as? Int)
+ else {
+ throw PluginError(message: "Invalid Arguments")
+ }
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let isManualEntry = recordingMethod == HealthConstants.RecordingMethod.manual.rawValue
+ let metadata = [
+ HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)
+ ]
+
+ let systolic_sample = HKQuantitySample(
+ type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!,
+ quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic),
+ start: dateFrom, end: dateTo, metadata: metadata)
+ let diastolic_sample = HKQuantitySample(
+ type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!,
+ quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic),
+ start: dateFrom, end: dateTo, metadata: metadata)
+ let bpCorrelationType = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!
+ let bpCorrelation = Set(arrayLiteral: systolic_sample, diastolic_sample)
+ let blood_pressure_sample = HKCorrelation(type: bpCorrelationType , start: dateFrom, end: dateTo, objects: bpCorrelation)
+
+ healthStore.save(
+ [blood_pressure_sample],
+ withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving Blood Pressure Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ }
+
+ /// Writes meal nutrition data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let name = (arguments["name"] as? String?),
+ let startTime = (arguments["start_time"] as? NSNumber),
+ let endTime = (arguments["end_time"] as? NSNumber),
+ let mealType = (arguments["meal_type"] as? String?),
+ let recordingMethod = arguments["recordingMethod"] as? Int
+ else {
+ throw PluginError(message: "Invalid Arguments")
+ }
+
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let mealTypeString = mealType ?? "UNKNOWN"
+
+ let isManualEntry = recordingMethod == HealthConstants.RecordingMethod.manual.rawValue
+
+ var metadata = ["HKFoodMeal": mealTypeString, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any]
+ if (name != nil) {
+ metadata[HKMetadataKeyFoodType] = "\(name!)"
+ }
+
+ var nutrition = Set()
+ for (key, identifier) in HealthConstants.NUTRITION_KEYS {
+ let value = arguments[key] as? Double
+ guard let unwrappedValue = value else { continue }
+ let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram()
+ let nutritionSample = HKQuantitySample(
+ type: HKSampleType.quantityType(forIdentifier: identifier)!, quantity: HKQuantity(unit: unit, doubleValue: unwrappedValue), start: dateFrom, end: dateTo, metadata: metadata)
+ nutrition.insert(nutritionSample)
+ }
+
+ if #available(iOS 15.0, *){
+ let type = HKCorrelationType.correlationType(forIdentifier: HKCorrelationTypeIdentifier.food)!
+ let meal = HKCorrelation(type: type, start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata)
+
+ healthStore.save(meal, withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving Meal Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ } else {
+ result(false)
+ }
+ }
+
+ /// Writes insulin delivery data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeInsulinDelivery(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let units = (arguments["units"] as? Double),
+ let reason = (arguments["reason"] as? NSNumber),
+ let startTime = (arguments["startTime"] as? NSNumber),
+ let endTime = (arguments["endTime"] as? NSNumber)
+ else {
+ throw PluginError(message: "Invalid Arguments")
+ }
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let type = HKSampleType.quantityType(forIdentifier: .insulinDelivery)!
+ let quantity = HKQuantity(unit: HKUnit.internationalUnit(), doubleValue: units)
+ let metadata = [HKMetadataKeyInsulinDeliveryReason: reason]
+
+ let insulin_sample = HKQuantitySample(type: type, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata)
+
+ healthStore.save(insulin_sample, withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving Insulin Delivery Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ }
+
+ /// Writes menstruation flow data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeMenstruationFlow(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let flow = (arguments["value"] as? Int),
+ let endTime = (arguments["endTime"] as? NSNumber),
+ let isStartOfCycle = (arguments["isStartOfCycle"] as? NSNumber),
+ let recordingMethod = (arguments["recordingMethod"] as? Int)
+ else {
+ throw PluginError(message: "Invalid Arguments - value, startTime, endTime or isStartOfCycle invalid")
+ }
+ guard let menstrualFlowType = HKCategoryValueMenstrualFlow(rawValue: flow) else {
+ throw PluginError(message: "Invalid Menstrual Flow Type")
+ }
+
+ let dateTime = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let isManualEntry = recordingMethod == HealthConstants.RecordingMethod.manual.rawValue
+
+ guard let categoryType = HKSampleType.categoryType(forIdentifier: .menstrualFlow) else {
+ throw PluginError(message: "Invalid Menstrual Flow Type")
+ }
+
+ let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any]
+
+ let sample = HKCategorySample(
+ type: categoryType,
+ value: menstrualFlowType.rawValue,
+ start: dateTime,
+ end: dateTime,
+ metadata: metadata
+ )
+
+ healthStore.save(
+ sample,
+ withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving Menstruation Flow Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ }
+
+ /// Writes workout data
+ /// - Parameters:
+ /// - call: Flutter method call
+ /// - result: Flutter result callback
+ func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
+ guard let arguments = call.arguments as? NSDictionary,
+ let activityType = (arguments["activityType"] as? String),
+ let startTime = (arguments["startTime"] as? NSNumber),
+ let endTime = (arguments["endTime"] as? NSNumber),
+ let activityTypeValue = workoutActivityTypeMap[activityType]
+ else {
+ throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid")
+ }
+
+ var totalEnergyBurned: HKQuantity?
+ var totalDistance: HKQuantity? = nil
+
+ // Handle optional arguments
+ if let teb = (arguments["totalEnergyBurned"] as? Double) {
+ totalEnergyBurned = HKQuantity(
+ unit: unitDict[(arguments["totalEnergyBurnedUnit"] as! String)]!, doubleValue: teb)
+ }
+ if let td = (arguments["totalDistance"] as? Double) {
+ totalDistance = HKQuantity(
+ unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td)
+ }
+
+ let dateFrom = HealthUtilities.dateFromMilliseconds(startTime.doubleValue)
+ let dateTo = HealthUtilities.dateFromMilliseconds(endTime.doubleValue)
+
+ let workout = HKWorkout(
+ activityType: activityTypeValue,
+ start: dateFrom,
+ end: dateTo,
+ duration: dateTo.timeIntervalSince(dateFrom),
+ totalEnergyBurned: totalEnergyBurned ?? nil,
+ totalDistance: totalDistance ?? nil,
+ metadata: nil
+ )
+
+ healthStore.save(
+ workout,
+ withCompletion: { (success, error) in
+ if let err = error {
+ print("Error Saving Workout. Sample: \(err.localizedDescription)")
+ }
+ DispatchQueue.main.async {
+ result(success)
+ }
+ })
+ }
+}
diff --git a/packages/health/ios/Classes/HealthUtilities.swift b/packages/health/ios/Classes/HealthUtilities.swift
new file mode 100644
index 000000000..6f4f961e9
--- /dev/null
+++ b/packages/health/ios/Classes/HealthUtilities.swift
@@ -0,0 +1,153 @@
+import HealthKit
+
+/// Utilities class containing helper methods for data manipulation
+class HealthUtilities {
+
+ /// Sanitize metadata to make it Flutter-friendly
+ /// - Parameter metadata: The metadata dictionary to sanitize
+ /// - Returns: A dictionary with sanitized values
+ static func sanitizeMetadata(_ metadata: [String: Any]?) -> [String: Any] {
+ guard let metadata = metadata else { return [:] }
+
+ var sanitized = [String: Any]()
+
+ for (key, value) in metadata {
+ switch value {
+ case let stringValue as String:
+ sanitized[key] = stringValue
+ case let numberValue as NSNumber:
+ sanitized[key] = numberValue
+ case let boolValue as Bool:
+ sanitized[key] = boolValue
+ case let arrayValue as [Any]:
+ sanitized[key] = sanitizeArray(arrayValue)
+ case let mapValue as [String: Any]:
+ sanitized[key] = sanitizeMetadata(mapValue)
+ default:
+ continue
+ }
+ }
+
+ return sanitized
+ }
+
+ /// Sanitize an array to make it Flutter-friendly
+ /// - Parameter array: The array to sanitize
+ /// - Returns: An array with sanitized values
+ static func sanitizeArray(_ array: [Any]) -> [Any] {
+ var sanitizedArray: [Any] = []
+
+ for value in array {
+ switch value {
+ case let stringValue as String:
+ sanitizedArray.append(stringValue)
+ case let numberValue as NSNumber:
+ sanitizedArray.append(numberValue)
+ case let boolValue as Bool:
+ sanitizedArray.append(boolValue)
+ case let arrayValue as [Any]:
+ sanitizedArray.append(sanitizeArray(arrayValue))
+ case let mapValue as [String: Any]:
+ sanitizedArray.append(sanitizeMetadata(mapValue))
+ default:
+ continue
+ }
+ }
+
+ return sanitizedArray
+ }
+
+ /// Convert milliseconds since epoch to Date
+ /// - Parameter milliseconds: Milliseconds since epoch
+ /// - Returns: Date object
+ static func dateFromMilliseconds(_ milliseconds: Double) -> Date {
+ return Date(timeIntervalSince1970: milliseconds / 1000)
+ }
+}
+
+/// Extension to provide type conversion helpers for HKWorkoutActivityType
+extension HKWorkoutActivityType {
+ /// Convert HKWorkoutActivityType to string
+ /// - Parameter type: The workout activity type
+ /// - Returns: String representation of the activity type
+ static func toString(_ type: HKWorkoutActivityType) -> String {
+ switch type {
+ case .americanFootball: return "americanFootball"
+ case .archery: return "archery"
+ case .australianFootball: return "australianFootball"
+ case .badminton: return "badminton"
+ case .baseball: return "baseball"
+ case .basketball: return "basketball"
+ case .bowling: return "bowling"
+ case .boxing: return "boxing"
+ case .climbing: return "climbing"
+ case .cricket: return "cricket"
+ case .crossTraining: return "crossTraining"
+ case .curling: return "curling"
+ case .cycling: return "cycling"
+ case .dance: return "dance"
+ case .danceInspiredTraining: return "danceInspiredTraining"
+ case .elliptical: return "elliptical"
+ case .equestrianSports: return "equestrianSports"
+ case .fencing: return "fencing"
+ case .fishing: return "fishing"
+ case .functionalStrengthTraining: return "functionalStrengthTraining"
+ case .golf: return "golf"
+ case .gymnastics: return "gymnastics"
+ case .handball: return "handball"
+ case .hiking: return "hiking"
+ case .hockey: return "hockey"
+ case .hunting: return "hunting"
+ case .lacrosse: return "lacrosse"
+ case .martialArts: return "martialArts"
+ case .mindAndBody: return "mindAndBody"
+ case .mixedMetabolicCardioTraining: return "mixedMetabolicCardioTraining"
+ case .paddleSports: return "paddleSports"
+ case .play: return "play"
+ case .preparationAndRecovery: return "preparationAndRecovery"
+ case .racquetball: return "racquetball"
+ case .rowing: return "rowing"
+ case .rugby: return "rugby"
+ case .running: return "running"
+ case .sailing: return "sailing"
+ case .skatingSports: return "skatingSports"
+ case .snowSports: return "snowSports"
+ case .soccer: return "soccer"
+ case .softball: return "softball"
+ case .squash: return "squash"
+ case .stairClimbing: return "stairClimbing"
+ case .surfingSports: return "surfingSports"
+ case .swimming: return "swimming"
+ case .tableTennis: return "tableTennis"
+ case .tennis: return "tennis"
+ case .trackAndField: return "trackAndField"
+ case .traditionalStrengthTraining: return "traditionalStrengthTraining"
+ case .volleyball: return "volleyball"
+ case .walking: return "walking"
+ case .waterFitness: return "waterFitness"
+ case .waterPolo: return "waterPolo"
+ case .waterSports: return "waterSports"
+ case .wrestling: return "wrestling"
+ case .yoga: return "yoga"
+ case .barre: return "barre"
+ case .coreTraining: return "coreTraining"
+ case .crossCountrySkiing: return "crossCountrySkiing"
+ case .downhillSkiing: return "downhillSkiing"
+ case .flexibility: return "flexibility"
+ case .highIntensityIntervalTraining: return "highIntensityIntervalTraining"
+ case .jumpRope: return "jumpRope"
+ case .kickboxing: return "kickboxing"
+ case .pilates: return "pilates"
+ case .snowboarding: return "snowboarding"
+ case .stairs: return "stairs"
+ case .stepTraining: return "stepTraining"
+ case .wheelchairWalkPace: return "wheelchairWalkPace"
+ case .wheelchairRunPace: return "wheelchairRunPace"
+ case .taiChi: return "taiChi"
+ case .mixedCardio: return "mixedCardio"
+ case .handCycling: return "handCycling"
+ case .underwaterDiving: return "underwaterDiving"
+ default: return "other"
+ }
+ }
+}
diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift
index 3a71a121e..881e2b6a7 100644
--- a/packages/health/ios/Classes/SwiftHealthPlugin.swift
+++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift
@@ -2,15 +2,10 @@ import Flutter
import HealthKit
import UIKit
-enum RecordingMethod: Int {
- case unknown = 0 // RECORDING_METHOD_UNKNOWN (not supported on iOS)
- case active = 1 // RECORDING_METHOD_ACTIVELY_RECORDED (not supported on iOS)
- case automatic = 2 // RECORDING_METHOD_AUTOMATICALLY_RECORDED
- case manual = 3 // RECORDING_METHOD_MANUAL_ENTRY
-}
-
+/// Main plugin class that coordinates health data operations
public class SwiftHealthPlugin: NSObject, FlutterPlugin {
+ // Health store and type dictionaries
let healthStore = HKHealthStore()
var healthDataTypes = [HKSampleType]()
var healthDataQuantityTypes = [HKQuantityType]()
@@ -25,203 +20,35 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
var characteristicsTypesDict: [String: HKCharacteristicType] = [:]
var nutritionList: [String] = []
- // Health Data Type Keys
- let ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED"
- let ATRIAL_FIBRILLATION_BURDEN = "ATRIAL_FIBRILLATION_BURDEN"
- let AUDIOGRAM = "AUDIOGRAM"
- let BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED"
- let BLOOD_GLUCOSE = "BLOOD_GLUCOSE"
- let BLOOD_OXYGEN = "BLOOD_OXYGEN"
- let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC"
- let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC"
- let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE"
- let LEAN_BODY_MASS = "LEAN_BODY_MASS"
- let BODY_MASS_INDEX = "BODY_MASS_INDEX"
- let BODY_TEMPERATURE = "BODY_TEMPERATURE"
- // Nutrition
- let DIETARY_CARBS_CONSUMED = "DIETARY_CARBS_CONSUMED"
- let DIETARY_ENERGY_CONSUMED = "DIETARY_ENERGY_CONSUMED"
- let DIETARY_FATS_CONSUMED = "DIETARY_FATS_CONSUMED"
- let DIETARY_PROTEIN_CONSUMED = "DIETARY_PROTEIN_CONSUMED"
- let DIETARY_CAFFEINE = "DIETARY_CAFFEINE"
- let DIETARY_FIBER = "DIETARY_FIBER"
- let DIETARY_SUGAR = "DIETARY_SUGAR"
- let DIETARY_FAT_MONOUNSATURATED = "DIETARY_FAT_MONOUNSATURATED"
- let DIETARY_FAT_POLYUNSATURATED = "DIETARY_FAT_POLYUNSATURATED"
- let DIETARY_FAT_SATURATED = "DIETARY_FAT_SATURATED"
- let DIETARY_CHOLESTEROL = "DIETARY_CHOLESTEROL"
- let DIETARY_VITAMIN_A = "DIETARY_VITAMIN_A"
- let DIETARY_THIAMIN = "DIETARY_THIAMIN"
- let DIETARY_RIBOFLAVIN = "DIETARY_RIBOFLAVIN"
- let DIETARY_NIACIN = "DIETARY_NIACIN"
- let DIETARY_PANTOTHENIC_ACID = "DIETARY_PANTOTHENIC_ACID"
- let DIETARY_VITAMIN_B6 = "DIETARY_VITAMIN_B6"
- let DIETARY_BIOTIN = "DIETARY_BIOTIN"
- let DIETARY_VITAMIN_B12 = "DIETARY_VITAMIN_B12"
- let DIETARY_VITAMIN_C = "DIETARY_VITAMIN_C"
- let DIETARY_VITAMIN_D = "DIETARY_VITAMIN_D"
- let DIETARY_VITAMIN_E = "DIETARY_VITAMIN_E"
- let DIETARY_VITAMIN_K = "DIETARY_VITAMIN_K"
- let DIETARY_FOLATE = "DIETARY_FOLATE"
- let DIETARY_CALCIUM = "DIETARY_CALCIUM"
- let DIETARY_CHLORIDE = "DIETARY_CHLORIDE"
- let DIETARY_IRON = "DIETARY_IRON"
- let DIETARY_MAGNESIUM = "DIETARY_MAGNESIUM"
- let DIETARY_PHOSPHORUS = "DIETARY_PHOSPHORUS"
- let DIETARY_POTASSIUM = "DIETARY_POTASSIUM"
- let DIETARY_SODIUM = "DIETARY_SODIUM"
- let DIETARY_ZINC = "DIETARY_ZINC"
- let DIETARY_WATER = "WATER"
- let DIETARY_CHROMIUM = "DIETARY_CHROMIUM"
- let DIETARY_COPPER = "DIETARY_COPPER"
- let DIETARY_IODINE = "DIETARY_IODINE"
- let DIETARY_MANGANESE = "DIETARY_MANGANESE"
- let DIETARY_MOLYBDENUM = "DIETARY_MOLYBDENUM"
- let DIETARY_SELENIUM = "DIETARY_SELENIUM"
- let NUTRITION_KEYS: [String: HKQuantityTypeIdentifier] = [
- "calories": .dietaryEnergyConsumed,
- "protein": .dietaryProtein,
- "carbs": .dietaryCarbohydrates,
- "fat": .dietaryFatTotal,
- "caffeine": .dietaryCaffeine,
- "vitamin_a": .dietaryVitaminA,
- "b1_thiamine": .dietaryThiamin,
- "b2_riboflavin": .dietaryRiboflavin,
- "b3_niacin" : .dietaryNiacin,
- "b5_pantothenic_acid" : .dietaryPantothenicAcid,
- "b6_pyridoxine" : .dietaryVitaminB6,
- "b7_biotin" : .dietaryBiotin,
- "b9_folate" : .dietaryFolate,
- "b12_cobalamin": .dietaryVitaminB12,
- "vitamin_c": .dietaryVitaminC,
- "vitamin_d": .dietaryVitaminD,
- "vitamin_e": .dietaryVitaminE,
- "vitamin_k": .dietaryVitaminK,
- "calcium": .dietaryCalcium,
- "chloride": .dietaryChloride,
- "cholesterol": .dietaryCholesterol,
- "chromium": .dietaryChromium,
- "copper": .dietaryCopper,
- "fat_unsaturated": .dietaryFatMonounsaturated,
- "fat_monounsaturated": .dietaryFatMonounsaturated,
- "fat_polyunsaturated": .dietaryFatPolyunsaturated,
- "fat_saturated": .dietaryFatSaturated,
- // "fat_trans_monoenoic": .dietaryFatTransMonoenoic,
- "fiber": .dietaryFiber,
- "iodine": .dietaryIodine,
- "iron": .dietaryIron,
- "magnesium": .dietaryMagnesium,
- "manganese": .dietaryManganese,
- "molybdenum": .dietaryMolybdenum,
- "phosphorus": .dietaryPhosphorus,
- "potassium": .dietaryPotassium,
- "selenium": .dietarySelenium,
- "sodium": .dietarySodium,
- "sugar": .dietarySugar,
- "water": .dietaryWater,
- "zinc": .dietaryZinc,
- ]
- let ELECTRODERMAL_ACTIVITY = "ELECTRODERMAL_ACTIVITY"
- let FORCED_EXPIRATORY_VOLUME = "FORCED_EXPIRATORY_VOLUME"
- let HEART_RATE = "HEART_RATE"
- let HEART_RATE_VARIABILITY_SDNN = "HEART_RATE_VARIABILITY_SDNN"
- let HEIGHT = "HEIGHT"
- let INSULIN_DELIVERY = "INSULIN_DELIVERY"
- let HIGH_HEART_RATE_EVENT = "HIGH_HEART_RATE_EVENT"
- let IRREGULAR_HEART_RATE_EVENT = "IRREGULAR_HEART_RATE_EVENT"
- let LOW_HEART_RATE_EVENT = "LOW_HEART_RATE_EVENT"
- let RESTING_HEART_RATE = "RESTING_HEART_RATE"
- let RESPIRATORY_RATE = "RESPIRATORY_RATE"
- let PERIPHERAL_PERFUSION_INDEX = "PERIPHERAL_PERFUSION_INDEX"
- let STEPS = "STEPS"
- let WAIST_CIRCUMFERENCE = "WAIST_CIRCUMFERENCE"
- let WALKING_HEART_RATE = "WALKING_HEART_RATE"
- let WEIGHT = "WEIGHT"
- let DISTANCE_WALKING_RUNNING = "DISTANCE_WALKING_RUNNING"
- let DISTANCE_SWIMMING = "DISTANCE_SWIMMING"
- let DISTANCE_CYCLING = "DISTANCE_CYCLING"
- let FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED"
- let MINDFULNESS = "MINDFULNESS"
- let SLEEP_ASLEEP = "SLEEP_ASLEEP"
- let SLEEP_AWAKE = "SLEEP_AWAKE"
- let SLEEP_DEEP = "SLEEP_DEEP"
- let SLEEP_IN_BED = "SLEEP_IN_BED"
- let SLEEP_LIGHT = "SLEEP_LIGHT"
- let SLEEP_REM = "SLEEP_REM"
-
- let EXERCISE_TIME = "EXERCISE_TIME"
- let WORKOUT = "WORKOUT"
- let HEADACHE_UNSPECIFIED = "HEADACHE_UNSPECIFIED"
- let HEADACHE_NOT_PRESENT = "HEADACHE_NOT_PRESENT"
- let HEADACHE_MILD = "HEADACHE_MILD"
- let HEADACHE_MODERATE = "HEADACHE_MODERATE"
- let HEADACHE_SEVERE = "HEADACHE_SEVERE"
- let ELECTROCARDIOGRAM = "ELECTROCARDIOGRAM"
- let NUTRITION = "NUTRITION"
- let BIRTH_DATE = "BIRTH_DATE"
- let GENDER = "GENDER"
- let BLOOD_TYPE = "BLOOD_TYPE"
- let MENSTRUATION_FLOW = "MENSTRUATION_FLOW"
- let WATER_TEMPERATURE = "WATER_TEMPERATURE"
- let UNDERWATER_DEPTH = "UNDERWATER_DEPTH"
- let UV_INDEX = "UV_INDEX"
-
-
- // Health Unit types
- // MOLE_UNIT_WITH_MOLAR_MASS, // requires molar mass input - not supported yet
- // MOLE_UNIT_WITH_PREFIX_MOLAR_MASS, // requires molar mass & prefix input - not supported yet
- let GRAM = "GRAM"
- let KILOGRAM = "KILOGRAM"
- let OUNCE = "OUNCE"
- let POUND = "POUND"
- let STONE = "STONE"
- let METER = "METER"
- let INCH = "INCH"
- let FOOT = "FOOT"
- let YARD = "YARD"
- let MILE = "MILE"
- let LITER = "LITER"
- let MILLILITER = "MILLILITER"
- let FLUID_OUNCE_US = "FLUID_OUNCE_US"
- let FLUID_OUNCE_IMPERIAL = "FLUID_OUNCE_IMPERIAL"
- let CUP_US = "CUP_US"
- let CUP_IMPERIAL = "CUP_IMPERIAL"
- let PINT_US = "PINT_US"
- let PINT_IMPERIAL = "PINT_IMPERIAL"
- let PASCAL = "PASCAL"
- let MILLIMETER_OF_MERCURY = "MILLIMETER_OF_MERCURY"
- let INCHES_OF_MERCURY = "INCHES_OF_MERCURY"
- let CENTIMETER_OF_WATER = "CENTIMETER_OF_WATER"
- let ATMOSPHERE = "ATMOSPHERE"
- let DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL = "DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL"
- let SECOND = "SECOND"
- let MILLISECOND = "MILLISECOND"
- let MINUTE = "MINUTE"
- let HOUR = "HOUR"
- let DAY = "DAY"
- let JOULE = "JOULE"
- let KILOCALORIE = "KILOCALORIE"
- let LARGE_CALORIE = "LARGE_CALORIE"
- let SMALL_CALORIE = "SMALL_CALORIE"
- let DEGREE_CELSIUS = "DEGREE_CELSIUS"
- let DEGREE_FAHRENHEIT = "DEGREE_FAHRENHEIT"
- let KELVIN = "KELVIN"
- let DECIBEL_HEARING_LEVEL = "DECIBEL_HEARING_LEVEL"
- let HERTZ = "HERTZ"
- let SIEMEN = "SIEMEN"
- let VOLT = "VOLT"
- let INTERNATIONAL_UNIT = "INTERNATIONAL_UNIT"
- let COUNT = "COUNT"
- let PERCENT = "PERCENT"
- let BEATS_PER_MINUTE = "BEATS_PER_MINUTE"
- let RESPIRATIONS_PER_MINUTE = "RESPIRATIONS_PER_MINUTE"
- let MILLIGRAM_PER_DECILITER = "MILLIGRAM_PER_DECILITER"
- let UNKNOWN_UNIT = "UNKNOWN_UNIT"
- let NO_UNIT = "NO_UNIT"
-
- struct PluginError: Error {
- let message: String
- }
+ // Service classes
+ private lazy var healthDataReader: HealthDataReader = {
+ return HealthDataReader(
+ healthStore: healthStore,
+ dataTypesDict: dataTypesDict,
+ dataQuantityTypesDict: dataQuantityTypesDict,
+ unitDict: unitDict,
+ workoutActivityTypeMap: workoutActivityTypeMap,
+ characteristicsTypesDict: characteristicsTypesDict
+ )
+ }()
+
+ private lazy var healthDataWriter: HealthDataWriter = {
+ return HealthDataWriter(
+ healthStore: healthStore,
+ dataTypesDict: dataTypesDict,
+ unitDict: unitDict,
+ workoutActivityTypeMap: workoutActivityTypeMap
+ )
+ }()
+
+ private lazy var healthDataOperations: HealthDataOperations = {
+ return HealthDataOperations(
+ healthStore: healthStore,
+ dataTypesDict: dataTypesDict,
+ characteristicsTypesDict: characteristicsTypesDict,
+ nutritionList: nutritionList
+ )
+ }()
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
@@ -231,1171 +58,486 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- // Set up all data types
- initializeTypes()
-
- /// Handle checkIfHealthDataAvailable
- if call.method.elementsEqual("checkIfHealthDataAvailable") {
- checkIfHealthDataAvailable(call: call, result: result)
- }/// Handle requestAuthorization
- else if call.method.elementsEqual("requestAuthorization") {
- try! requestAuthorization(call: call, result: result)
- }
-
- /// Handle getData
- else if call.method.elementsEqual("getData") {
- getData(call: call, result: result)
- }
-
- /// Handle getIntervalData
- else if (call.method.elementsEqual("getIntervalData")){
- getIntervalData(call: call, result: result)
- }
-
- /// Handle getTotalStepsInInterval
- else if call.method.elementsEqual("getTotalStepsInInterval") {
- getTotalStepsInInterval(call: call, result: result)
- }
-
- /// Handle writeData
- else if call.method.elementsEqual("writeData") {
- try! writeData(call: call, result: result)
- }
-
- /// Handle writeAudiogram
- else if call.method.elementsEqual("writeAudiogram") {
- try! writeAudiogram(call: call, result: result)
- }
-
- /// Handle writeBloodPressure
- else if call.method.elementsEqual("writeBloodPressure") {
- try! writeBloodPressure(call: call, result: result)
- }
-
- /// Handle writeMeal
- else if (call.method.elementsEqual("writeMeal")){
- try! writeMeal(call: call, result: result)
- }
-
- /// Handle writeInsulinDelivery
- else if (call.method.elementsEqual("writeInsulinDelivery")){
- try! writeInsulinDelivery(call: call, result: result)
- }
-
- /// Handle writeWorkoutData
- else if call.method.elementsEqual("writeWorkoutData") {
- try! writeWorkoutData(call: call, result: result)
- }
-
- /// Handle writeMenstruationFlow
- else if call.method.elementsEqual("writeMenstruationFlow") {
- try! writeMenstruationFlow(call: call, result: result)
- }
-
- /// Handle hasPermission
- else if call.method.elementsEqual("hasPermissions") {
- try! hasPermissions(call: call, result: result)
- }
- /// Handle delete data
- else if call.method.elementsEqual("delete") {
- try! delete(call: call, result: result)
- }
-
- /// Handle deleteByUUID data
- else if call.method.elementsEqual("deleteByUUID") {
- try! deleteByUUID(call: call, result: result)
- }
- }
-
- func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) {
- result(HKHealthStore.isHealthDataAvailable())
- }
-
- func hasPermissions(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- let arguments = call.arguments as? NSDictionary
- guard var types = arguments?["types"] as? [String],
- var permissions = arguments?["permissions"] as? [Int],
- types.count == permissions.count
- else {
- throw PluginError(message: "Invalid Arguments!")
- }
+ initializeTypes()
- if let nutritionIndex = types.firstIndex(of: NUTRITION) {
- types.remove(at: nutritionIndex)
- let nutritionPermission = permissions[nutritionIndex]
- permissions.remove(at: nutritionIndex)
+ switch call.method {
+ case "checkIfHealthDataAvailable":
+ healthDataOperations.checkIfHealthDataAvailable(call: call, result: result)
- for nutritionType in nutritionList {
- types.append(nutritionType)
- permissions.append(nutritionPermission)
+ case "requestAuthorization":
+ do {
+ try healthDataOperations.requestAuthorization(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "REQUEST_AUTH_ERROR",
+ message: "Error requesting authorization: \(error.localizedDescription)",
+ details: nil))
}
- }
-
- for (index, type) in types.enumerated() {
- let sampleType = dataTypeLookUp(key: type)
- let success = hasPermission(type: sampleType, access: permissions[index])
- if success == nil || success == false {
- result(success)
- return
- }
- if let characteristicType = characteristicsTypesDict[type] {
- let characteristicSuccess = hasPermission(type: characteristicType, access: permissions[index])
- if (characteristicSuccess == nil || characteristicSuccess == false) {
- result(characteristicSuccess)
- return
- }
- }
- }
-
- result(true)
- }
-
- func hasPermission(type: HKObjectType, access: Int) -> Bool? {
-
- if #available(iOS 13.0, *) {
- let status = healthStore.authorizationStatus(for: type)
- switch access {
- case 0: // READ
- return nil
- case 1: // WRITE
- return (status == HKAuthorizationStatus.sharingAuthorized)
- default: // READ_WRITE
- return nil
- }
- } else {
- return nil
- }
- }
-
- func requestAuthorization(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let types = arguments["types"] as? [String],
- let permissions = arguments["permissions"] as? [Int],
- permissions.count == types.count
- else {
- throw PluginError(message: "Invalid Arguments!")
- }
-
- var typesToRead = Set()
- var typesToWrite = Set()
- for (index, key) in types.enumerated() {
- if (key == NUTRITION) {
- for nutritionType in nutritionList {
- let nutritionData = dataTypeLookUp(key: nutritionType)
- typesToWrite.insert(nutritionData)
- }
- } else {
- let dataType = dataTypeLookUp(key: key)
- let access = permissions[index]
- switch access {
- case 0:
- typesToRead.insert(dataType)
- case 1:
- typesToWrite.insert(dataType)
- default:
- typesToRead.insert(dataType)
- typesToWrite.insert(dataType)
- }
- if let characteristicsType = characteristicsTypesDict[key] {
- let access = permissions[index]
- switch access {
- case 0:
- typesToRead.insert(characteristicsType)
- case 1:
- throw PluginError(message: "Can not ask for reading permissions to the type of \(characteristicsType)")
- default:
- break
- }
- }
+
+ case "getData":
+ healthDataReader.getData(call: call, result: result)
+
+ case "getIntervalData":
+ healthDataReader.getIntervalData(call: call, result: result)
+
+ case "getTotalStepsInInterval":
+ healthDataReader.getTotalStepsInInterval(call: call, result: result)
+
+ case "writeData":
+ do {
+ try healthDataWriter.writeData(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing data: \(error.localizedDescription)",
+ details: nil))
}
- }
-
- if #available(iOS 13.0, *) {
- healthStore.requestAuthorization(toShare: typesToWrite, read: typesToRead) {
- (success, error) in
- DispatchQueue.main.async {
- result(success)
- }
+
+ case "writeAudiogram":
+ do {
+ try healthDataWriter.writeAudiogram(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing audiogram: \(error.localizedDescription)",
+ details: nil))
}
- } else {
- result(false) // Handle the error here.
- }
- }
-
- func writeData(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let value = (arguments["value"] as? Double),
- let type = (arguments["dataTypeKey"] as? String),
- let unit = (arguments["dataUnitKey"] as? String),
- let startTime = (arguments["startTime"] as? NSNumber),
- let endTime = (arguments["endTime"] as? NSNumber),
- let recordingMethod = (arguments["recordingMethod"] as? Int)
- else {
- throw PluginError(message: "Invalid Arguments")
- }
-
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue
- let metadata: [String: Any] = [
- HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)
- ]
-
- let sample: HKObject
-
- if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) {
- sample = HKCategorySample(
- type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom,
- end: dateTo, metadata: metadata)
- } else {
- let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value)
- sample = HKQuantitySample(
- type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom,
- end: dateTo, metadata: metadata)
- }
-
- HKHealthStore().save(
- sample,
- withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving \(type) Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
- })
- }
-
- func writeAudiogram(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let frequencies = (arguments["frequencies"] as? [Double]),
- let leftEarSensitivities = (arguments["leftEarSensitivities"] as? [Double]),
- let rightEarSensitivities = (arguments["rightEarSensitivities"] as? [Double]),
- let startTime = (arguments["startTime"] as? NSNumber),
- let endTime = (arguments["endTime"] as? NSNumber)
- else {
- throw PluginError(message: "Invalid Arguments")
- }
-
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- var sensitivityPoints = [HKAudiogramSensitivityPoint]()
-
- for index in 0...frequencies.count - 1 {
- let frequency = HKQuantity(unit: HKUnit.hertz(), doubleValue: frequencies[index])
- let dbUnit = HKUnit.decibelHearingLevel()
- let left = HKQuantity(unit: dbUnit, doubleValue: leftEarSensitivities[index])
- let right = HKQuantity(unit: dbUnit, doubleValue: rightEarSensitivities[index])
- let sensitivityPoint = try HKAudiogramSensitivityPoint(
- frequency: frequency, leftEarSensitivity: left, rightEarSensitivity: right)
- sensitivityPoints.append(sensitivityPoint)
- }
-
- let audiogram: HKAudiogramSample
- let metadataReceived = (arguments["metadata"] as? [String: Any]?)
-
- if (metadataReceived) != nil {
- guard let deviceName = metadataReceived?!["HKDeviceName"] as? String else { return }
- guard let externalUUID = metadataReceived?!["HKExternalUUID"] as? String else { return }
- audiogram = HKAudiogramSample(
- sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo,
- metadata: [HKMetadataKeyDeviceName: deviceName, HKMetadataKeyExternalUUID: externalUUID])
+ case "writeBloodPressure":
+ do {
+ try healthDataWriter.writeBloodPressure(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing blood pressure: \(error.localizedDescription)",
+ details: nil))
+ }
- } else {
- audiogram = HKAudiogramSample(
- sensitivityPoints: sensitivityPoints, start: dateFrom, end: dateTo, metadata: nil)
- }
-
- HKHealthStore().save(
- audiogram,
- withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving Audiogram. Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
- })
- }
-
- func writeBloodPressure(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let systolic = (arguments["systolic"] as? Double),
- let diastolic = (arguments["diastolic"] as? Double),
- let startTime = (arguments["startTime"] as? NSNumber),
- let endTime = (arguments["endTime"] as? NSNumber),
- let recordingMethod = (arguments["recordingMethod"] as? Int)
- else {
- throw PluginError(message: "Invalid Arguments")
- }
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue
- let metadata = [
- HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)
- ]
-
- let systolic_sample = HKQuantitySample(
- type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!,
- quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic),
- start: dateFrom, end: dateTo, metadata: metadata)
- let diastolic_sample = HKQuantitySample(
- type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!,
- quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic),
- start: dateFrom, end: dateTo, metadata: metadata)
- let bpCorrelationType = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!
- let bpCorrelation = Set(arrayLiteral: systolic_sample, diastolic_sample)
- let blood_pressure_sample = HKCorrelation(type: bpCorrelationType , start: dateFrom, end: dateTo, objects: bpCorrelation)
-
- HKHealthStore().save(
- [blood_pressure_sample],
- withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving Blood Pressure Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
- })
- }
-
- func writeMeal(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let name = (arguments["name"] as? String?),
- let startTime = (arguments["start_time"] as? NSNumber),
- let endTime = (arguments["end_time"] as? NSNumber),
- let mealType = (arguments["meal_type"] as? String?),
- let recordingMethod = arguments["recordingMethod"] as? Int
- else {
- throw PluginError(message: "Invalid Arguments")
- }
-
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let mealTypeString = mealType ?? "UNKNOWN"
-
- let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue
-
- var metadata = ["HKFoodMeal": mealTypeString, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any]
- if (name != nil) {
- metadata[HKMetadataKeyFoodType] = "\(name!)"
- }
-
- var nutrition = Set()
- for (key, identifier) in NUTRITION_KEYS {
- let value = arguments[key] as? Double
- guard let unwrappedValue = value else { continue }
- let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram()
- let nutritionSample = HKQuantitySample(
- type: HKSampleType.quantityType(forIdentifier: identifier)!, quantity: HKQuantity(unit: unit, doubleValue: unwrappedValue), start: dateFrom, end: dateTo, metadata: metadata)
- nutrition.insert(nutritionSample)
- }
-
- if #available(iOS 15.0, *){
- let type = HKCorrelationType.correlationType(forIdentifier: HKCorrelationTypeIdentifier.food)!
- let meal = HKCorrelation(type: type, start: dateFrom, end: dateTo, objects: nutrition, metadata: metadata)
+ case "writeMeal":
+ do {
+ try healthDataWriter.writeMeal(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing meal: \(error.localizedDescription)",
+ details: nil))
+ }
- HKHealthStore().save(meal, withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving Meal Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
- })
- } else {
- result(false)
- }
- }
- func writeInsulinDelivery(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let units = (arguments["units"] as? Double),
- let reason = (arguments["reason"] as? NSNumber),
- let startTime = (arguments["startTime"] as? NSNumber),
- let endTime = (arguments["endTime"] as? NSNumber)
- else {
- throw PluginError(message: "Invalid Arguments")
- }
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let type = HKSampleType.quantityType(forIdentifier: .insulinDelivery)!
- let quantity = HKQuantity(unit: HKUnit.internationalUnit(), doubleValue: units)
- let metadata = [HKMetadataKeyInsulinDeliveryReason: reason]
-
- let insulin_sample = HKQuantitySample(type: type, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata)
-
- HKHealthStore().save(insulin_sample, withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving Insulin Delivery Sample: \(err.localizedDescription)")
+ case "writeInsulinDelivery":
+ do {
+ try healthDataWriter.writeInsulinDelivery(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing insulin delivery: \(error.localizedDescription)",
+ details: nil))
}
- DispatchQueue.main.async {
- result(success)
+
+ case "writeWorkoutData":
+ do {
+ try healthDataWriter.writeWorkoutData(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing workout: \(error.localizedDescription)",
+ details: nil))
}
- })
- }
-
- func writeMenstruationFlow(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let flow = (arguments["value"] as? Int),
- let endTime = (arguments["endTime"] as? NSNumber),
- let isStartOfCycle = (arguments["isStartOfCycle"] as? NSNumber),
- let recordingMethod = (arguments["recordingMethod"] as? Int)
- else {
- throw PluginError(message: "Invalid Arguments - value, startTime, endTime or isStartOfCycle invalid")
- }
- guard let menstrualFlowType = HKCategoryValueMenstrualFlow(rawValue: flow) else {
- throw PluginError(message: "Invalid Menstrual Flow Type")
- }
-
- let dateTime = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue
-
- guard let categoryType = HKSampleType.categoryType(forIdentifier: .menstrualFlow) else {
- throw PluginError(message: "Invalid Menstrual Flow Type")
- }
-
- let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any]
-
- let sample = HKCategorySample(
- type: categoryType,
- value: menstrualFlowType.rawValue,
- start: dateTime,
- end: dateTime,
- metadata: metadata
- )
-
- HKHealthStore().save(
- sample,
- withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving Menstruation Flow Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
- })
- }
-
- func writeWorkoutData(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let activityType = (arguments["activityType"] as? String),
- let startTime = (arguments["startTime"] as? NSNumber),
- let endTime = (arguments["endTime"] as? NSNumber),
- let ac = workoutActivityTypeMap[activityType]
- else {
- throw PluginError(message: "Invalid Arguments - activityType, startTime or endTime invalid")
- }
-
- var totalEnergyBurned: HKQuantity?
- var totalDistance: HKQuantity? = nil
-
- // Handle optional arguments
- if let teb = (arguments["totalEnergyBurned"] as? Double) {
- totalEnergyBurned = HKQuantity(
- unit: unitDict[(arguments["totalEnergyBurnedUnit"] as! String)]!, doubleValue: teb)
- }
- if let td = (arguments["totalDistance"] as? Double) {
- totalDistance = HKQuantity(
- unit: unitDict[(arguments["totalDistanceUnit"] as! String)]!, doubleValue: td)
- }
-
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- var workout: HKWorkout
-
- workout = HKWorkout(
- activityType: ac, start: dateFrom, end: dateTo, duration: dateTo.timeIntervalSince(dateFrom),
- totalEnergyBurned: totalEnergyBurned ?? nil,
- totalDistance: totalDistance ?? nil, metadata: nil)
-
- HKHealthStore().save(
- workout,
- withCompletion: { (success, error) in
- if let err = error {
- print("Error Saving Workout. Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
- })
- }
-
- func delete(call: FlutterMethodCall, result: @escaping FlutterResult) {
- let arguments = call.arguments as? NSDictionary
- let dataTypeKey = (arguments?["dataTypeKey"] as? String)!
- let startTime = (arguments?["startTime"] as? NSNumber) ?? 0
- let endTime = (arguments?["endTime"] as? NSNumber) ?? 0
-
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let dataType = dataTypeLookUp(key: dataTypeKey)
-
- let samplePredicate = HKQuery.predicateForSamples(
- withStart: dateFrom, end: dateTo, options: .strictStartDate)
- let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default())
- let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
-
- let deleteQuery = HKSampleQuery(
- sampleType: dataType,
- predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]),
- limit: HKObjectQueryNoLimit,
- sortDescriptors: [sortDescriptor]
- ) { [self] x, samplesOrNil, error in
- guard let samplesOrNil = samplesOrNil, error == nil else {
- // Handle the error if necessary
- print("Error deleting \(dataType)")
- return
+ case "writeMenstruationFlow":
+ do {
+ try healthDataWriter.writeMenstruationFlow(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "WRITE_ERROR",
+ message: "Error writing menstruation flow: \(error.localizedDescription)",
+ details: nil))
}
- // Delete the retrieved objects from the HealthKit store
- HKHealthStore().delete(samplesOrNil) { (success, error) in
- if let err = error {
- print("Error deleting \(dataType) Sample: \(err.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
+ case "hasPermissions":
+ do {
+ try healthDataOperations.hasPermissions(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "PERMISSION_ERROR",
+ message: "Error checking permissions: \(error.localizedDescription)",
+ details: nil))
}
- }
-
- HKHealthStore().execute(deleteQuery)
- }
-
- func deleteByUUID(call: FlutterMethodCall, result: @escaping FlutterResult) throws {
- guard let arguments = call.arguments as? NSDictionary,
- let uuidarg = arguments["uuid"] as? String,
- let dataTypeKey = arguments["dataTypeKey"] as? String else {
- throw PluginError(message: "Invalid Arguments - UUID or DataTypeKey invalid")
- }
- let dataTypeToRemove = dataTypeLookUp(key: dataTypeKey)
- guard let uuid = UUID(uuidString: uuidarg) else {
- result(false)
- return
- }
- let predicate = HKQuery.predicateForObjects(with: [uuid])
-
- let query = HKSampleQuery(
- sampleType: dataTypeToRemove,
- predicate: predicate,
- limit: 1,
- sortDescriptors: nil
- ) { query, samplesOrNil, error in
- guard let samples = samplesOrNil, !samples.isEmpty else {
- DispatchQueue.main.async {
- result(false)
- }
- return
+
+ case "delete":
+ do {
+ healthDataOperations.delete(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "DELETE_ERROR",
+ message: "Error deleting data: \(error.localizedDescription)",
+ details: nil))
}
- self.healthStore.delete(samples) { success, error in
- if let error = error {
- print("Error deleting sample with UUID \(uuid): \(error.localizedDescription)")
- }
- DispatchQueue.main.async {
- result(success)
- }
+ case "deleteByUUID":
+ do {
+ try healthDataOperations.deleteByUUID(call: call, result: result)
+ } catch {
+ result(FlutterError(code: "DELETE_ERROR",
+ message: "Error deleting data by UUID: \(error.localizedDescription)",
+ details: nil))
}
+
+ default:
+ result(FlutterMethodNotImplemented)
}
-
- healthStore.execute(query)
}
- func getData(call: FlutterMethodCall, result: @escaping FlutterResult) {
- let arguments = call.arguments as? NSDictionary
- let dataTypeKey = (arguments?["dataTypeKey"] as? String)!
- let dataUnitKey = (arguments?["dataUnitKey"] as? String)
- let startTime = (arguments?["startTime"] as? NSNumber) ?? 0
- let endTime = (arguments?["endTime"] as? NSNumber) ?? 0
- let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit
- let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? []
- let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue)
+ /// Initialize all the health data types, unit dictionaries, and other required data structures
+ func initializeTypes() {
+ // init units
+ unitDict[HealthConstants.GRAM] = HKUnit.gram()
+ unitDict[HealthConstants.KILOGRAM] = HKUnit.gramUnit(with: .kilo)
+ unitDict[HealthConstants.OUNCE] = HKUnit.ounce()
+ unitDict[HealthConstants.POUND] = HKUnit.pound()
+ unitDict[HealthConstants.STONE] = HKUnit.stone()
+ unitDict[HealthConstants.METER] = HKUnit.meter()
+ unitDict[HealthConstants.INCH] = HKUnit.inch()
+ unitDict[HealthConstants.FOOT] = HKUnit.foot()
+ unitDict[HealthConstants.YARD] = HKUnit.yard()
+ unitDict[HealthConstants.MILE] = HKUnit.mile()
+ unitDict[HealthConstants.LITER] = HKUnit.liter()
+ unitDict[HealthConstants.MILLILITER] = HKUnit.literUnit(with: .milli)
+ unitDict[HealthConstants.FLUID_OUNCE_US] = HKUnit.fluidOunceUS()
+ unitDict[HealthConstants.FLUID_OUNCE_IMPERIAL] = HKUnit.fluidOunceImperial()
+ unitDict[HealthConstants.CUP_US] = HKUnit.cupUS()
+ unitDict[HealthConstants.CUP_IMPERIAL] = HKUnit.cupImperial()
+ unitDict[HealthConstants.PINT_US] = HKUnit.pintUS()
+ unitDict[HealthConstants.PINT_IMPERIAL] = HKUnit.pintImperial()
+ unitDict[HealthConstants.PASCAL] = HKUnit.pascal()
+ unitDict[HealthConstants.MILLIMETER_OF_MERCURY] = HKUnit.millimeterOfMercury()
+ unitDict[HealthConstants.CENTIMETER_OF_WATER] = HKUnit.centimeterOfWater()
+ unitDict[HealthConstants.ATMOSPHERE] = HKUnit.atmosphere()
+ unitDict[HealthConstants.DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL] = HKUnit.decibelAWeightedSoundPressureLevel()
+ unitDict[HealthConstants.SECOND] = HKUnit.second()
+ unitDict[HealthConstants.MILLISECOND] = HKUnit.secondUnit(with: .milli)
+ unitDict[HealthConstants.MINUTE] = HKUnit.minute()
+ unitDict[HealthConstants.HOUR] = HKUnit.hour()
+ unitDict[HealthConstants.DAY] = HKUnit.day()
+ unitDict[HealthConstants.JOULE] = HKUnit.joule()
+ unitDict[HealthConstants.KILOCALORIE] = HKUnit.kilocalorie()
+ unitDict[HealthConstants.LARGE_CALORIE] = HKUnit.largeCalorie()
+ unitDict[HealthConstants.SMALL_CALORIE] = HKUnit.smallCalorie()
+ unitDict[HealthConstants.DEGREE_CELSIUS] = HKUnit.degreeCelsius()
+ unitDict[HealthConstants.DEGREE_FAHRENHEIT] = HKUnit.degreeFahrenheit()
+ unitDict[HealthConstants.KELVIN] = HKUnit.kelvin()
+ unitDict[HealthConstants.DECIBEL_HEARING_LEVEL] = HKUnit.decibelHearingLevel()
+ unitDict[HealthConstants.HERTZ] = HKUnit.hertz()
+ unitDict[HealthConstants.SIEMEN] = HKUnit.siemen()
+ unitDict[HealthConstants.INTERNATIONAL_UNIT] = HKUnit.internationalUnit()
+ unitDict[HealthConstants.COUNT] = HKUnit.count()
+ unitDict[HealthConstants.PERCENT] = HKUnit.percent()
+ unitDict[HealthConstants.BEATS_PER_MINUTE] = HKUnit.init(from: "count/min")
+ unitDict[HealthConstants.RESPIRATIONS_PER_MINUTE] = HKUnit.init(from: "count/min")
+ unitDict[HealthConstants.MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL")
+ unitDict[HealthConstants.UNKNOWN_UNIT] = HKUnit.init(from: "")
+ unitDict[HealthConstants.NO_UNIT] = HKUnit.init(from: "")
+
+ // init workout activity types
+ initializeWorkoutTypes()
- // Convert dates from milliseconds to Date()
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
+ nutritionList = [
+ HealthConstants.DIETARY_ENERGY_CONSUMED,
+ HealthConstants.DIETARY_CARBS_CONSUMED,
+ HealthConstants.DIETARY_PROTEIN_CONSUMED,
+ HealthConstants.DIETARY_FATS_CONSUMED,
+ HealthConstants.DIETARY_CAFFEINE,
+ HealthConstants.DIETARY_FIBER,
+ HealthConstants.DIETARY_SUGAR,
+ HealthConstants.DIETARY_FAT_MONOUNSATURATED,
+ HealthConstants.DIETARY_FAT_POLYUNSATURATED,
+ HealthConstants.DIETARY_FAT_SATURATED,
+ HealthConstants.DIETARY_CHOLESTEROL,
+ HealthConstants.DIETARY_VITAMIN_A,
+ HealthConstants.DIETARY_THIAMIN,
+ HealthConstants.DIETARY_RIBOFLAVIN,
+ HealthConstants.DIETARY_NIACIN,
+ HealthConstants.DIETARY_PANTOTHENIC_ACID,
+ HealthConstants.DIETARY_VITAMIN_B6,
+ HealthConstants.DIETARY_BIOTIN,
+ HealthConstants.DIETARY_VITAMIN_B12,
+ HealthConstants.DIETARY_VITAMIN_C,
+ HealthConstants.DIETARY_VITAMIN_D,
+ HealthConstants.DIETARY_VITAMIN_E,
+ HealthConstants.DIETARY_VITAMIN_K,
+ HealthConstants.DIETARY_FOLATE,
+ HealthConstants.DIETARY_CALCIUM,
+ HealthConstants.DIETARY_CHLORIDE,
+ HealthConstants.DIETARY_IRON,
+ HealthConstants.DIETARY_MAGNESIUM,
+ HealthConstants.DIETARY_PHOSPHORUS,
+ HealthConstants.DIETARY_POTASSIUM,
+ HealthConstants.DIETARY_SODIUM,
+ HealthConstants.DIETARY_ZINC,
+ HealthConstants.DIETARY_WATER,
+ HealthConstants.DIETARY_CHROMIUM,
+ HealthConstants.DIETARY_COPPER,
+ HealthConstants.DIETARY_IODINE,
+ HealthConstants.DIETARY_MANGANESE,
+ HealthConstants.DIETARY_MOLYBDENUM,
+ HealthConstants.DIETARY_SELENIUM,
+ ]
- let dataType = dataTypeLookUp(key: dataTypeKey)
- var unit: HKUnit?
- if let dataUnitKey = dataUnitKey {
- unit = unitDict[dataUnitKey]
+ // Set up iOS 13 specific types (ordinary health data types)
+ if #available(iOS 13.0, *) {
+ initializeIOS13Types()
+ healthDataTypes = Array(dataTypesDict.values)
+ characteristicsDataTypes = Array(characteristicsTypesDict.values)
}
- let sourceIdForCharacteristic = "com.apple.Health"
- let sourceNameForCharacteristic = "Health"
-
- switch(dataTypeKey) {
- case "BIRTH_DATE":
- let dateOfBirth = getBirthDate()
- result([
- [
- "value": dateOfBirth?.timeIntervalSince1970,
- "date_from": Int(dateFrom.timeIntervalSince1970 * 1000),
- "date_to": Int(dateTo.timeIntervalSince1970 * 1000),
- "source_id": sourceIdForCharacteristic,
- "source_name": sourceNameForCharacteristic,
- "recording_method": RecordingMethod.manual.rawValue
- ]
- ])
- return
- case "GENDER":
- let gender = getGender()
- result([
- [
- "value": gender?.rawValue,
- "date_from": Int(dateFrom.timeIntervalSince1970 * 1000),
- "date_to": Int(dateTo.timeIntervalSince1970 * 1000),
- "source_id": sourceIdForCharacteristic,
- "source_name": sourceNameForCharacteristic,
- "recording_method": RecordingMethod.manual.rawValue
- ]
- ])
- return
- case "BLOOD_TYPE":
- let bloodType = getBloodType()
- result([
- [
- "value": bloodType?.rawValue,
- "date_from": Int(dateFrom.timeIntervalSince1970 * 1000),
- "date_to": Int(dateTo.timeIntervalSince1970 * 1000),
- "source_id": sourceIdForCharacteristic,
- "source_name": sourceNameForCharacteristic,
- "recording_method": RecordingMethod.manual.rawValue
- ]
- ])
- return
- default:
- break
+ // Set up iOS 11 specific types (ordinary health data quantity types)
+ if #available(iOS 11.0, *) {
+ initializeIOS11Types()
+ healthDataQuantityTypes = Array(dataQuantityTypesDict.values)
}
- var predicate = HKQuery.predicateForSamples(
- withStart: dateFrom, end: dateTo, options: .strictStartDate)
- if (!includeManualEntry) {
- let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
- predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
+ // Set up heart rate data types specific to the apple watch, requires iOS 12
+ if #available(iOS 12.2, *) {
+ initializeIOS12Types()
}
- let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
- let query = HKSampleQuery(
- sampleType: dataType, predicate: predicate, limit: limit, sortDescriptors: [sortDescriptor]
- ) {
- [self]
- x, samplesOrNil, error in
-
- switch samplesOrNil {
- case let (samples as [HKQuantitySample]) as Any:
- let dictionaries = samples.map { sample -> NSDictionary in
- return [
- "uuid": "\(sample.uuid)",
- "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()),
- "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
- "source_id": sample.sourceRevision.source.bundleIdentifier,
- "source_name": sample.sourceRevision.source.name,
- "device_model": sample.device?.model ?? "unknown",
- "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
- ? RecordingMethod.manual.rawValue
- : RecordingMethod.automatic.rawValue,
- "dataUnitKey": unit?.unitString,
- "metadata": sanitizeMetadata(sample.metadata)
- ]
- }
- DispatchQueue.main.async {
- result(dictionaries)
- }
-
- case var (samplesCategory as [HKCategorySample]) as Any:
-
- if dataTypeKey == self.SLEEP_IN_BED {
- samplesCategory = samplesCategory.filter { $0.value == 0 }
- }
- if dataTypeKey == self.SLEEP_ASLEEP {
- samplesCategory = samplesCategory.filter { $0.value == 1 }
- }
- if dataTypeKey == self.SLEEP_AWAKE {
- samplesCategory = samplesCategory.filter { $0.value == 2 }
- }
- if dataTypeKey == self.SLEEP_LIGHT {
- samplesCategory = samplesCategory.filter { $0.value == 3 }
- }
- if dataTypeKey == self.SLEEP_DEEP {
- samplesCategory = samplesCategory.filter { $0.value == 4 }
- }
- if dataTypeKey == self.SLEEP_REM {
- samplesCategory = samplesCategory.filter { $0.value == 5 }
- }
- if dataTypeKey == self.HEADACHE_UNSPECIFIED {
- samplesCategory = samplesCategory.filter { $0.value == 0 }
- }
- if dataTypeKey == self.HEADACHE_NOT_PRESENT {
- samplesCategory = samplesCategory.filter { $0.value == 1 }
- }
- if dataTypeKey == self.HEADACHE_MILD {
- samplesCategory = samplesCategory.filter { $0.value == 2 }
- }
- if dataTypeKey == self.HEADACHE_MODERATE {
- samplesCategory = samplesCategory.filter { $0.value == 3 }
- }
- if dataTypeKey == self.HEADACHE_SEVERE {
- samplesCategory = samplesCategory.filter { $0.value == 4 }
- }
- let categories = samplesCategory.map { sample -> NSDictionary in
- return [
- "uuid": "\(sample.uuid)",
- "value": sample.value,
- "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
- "source_id": sample.sourceRevision.source.bundleIdentifier,
- "source_name": sample.sourceRevision.source.name,
- "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue,
- "metadata": sanitizeMetadata(sample.metadata)
- ]
- }
- DispatchQueue.main.async {
- result(categories)
- }
-
- case let (samplesWorkout as [HKWorkout]) as Any:
-
- let dictionaries = samplesWorkout.map { sample -> NSDictionary in
- return [
- "uuid": "\(sample.uuid)",
- "workoutActivityType": workoutActivityTypeMap.first(where: {
- $0.value == sample.workoutActivityType
- })?.key,
- "totalEnergyBurned": sample.totalEnergyBurned?.doubleValue(for: HKUnit.kilocalorie()),
- "totalEnergyBurnedUnit": "KILOCALORIE",
- "totalDistance": sample.totalDistance?.doubleValue(for: HKUnit.meter()),
- "totalDistanceUnit": "METER",
- "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
- "source_id": sample.sourceRevision.source.bundleIdentifier,
- "source_name": sample.sourceRevision.source.name,
- "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue,
- "workout_type": self.getWorkoutType(type: sample.workoutActivityType),
- "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0,
- "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0
- ]
- }
-
- DispatchQueue.main.async {
- result(dictionaries)
- }
-
- case let (samplesAudiogram as [HKAudiogramSample]) as Any:
- let dictionaries = samplesAudiogram.map { sample -> NSDictionary in
- var frequencies = [Double]()
- var leftEarSensitivities = [Double]()
- var rightEarSensitivities = [Double]()
- for samplePoint in sample.sensitivityPoints {
- frequencies.append(samplePoint.frequency.doubleValue(for: HKUnit.hertz()))
- leftEarSensitivities.append(
- samplePoint.leftEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel()))
- rightEarSensitivities.append(
- samplePoint.rightEarSensitivity!.doubleValue(for: HKUnit.decibelHearingLevel()))
- }
- return [
- "uuid": "\(sample.uuid)",
- "frequencies": frequencies,
- "leftEarSensitivities": leftEarSensitivities,
- "rightEarSensitivities": rightEarSensitivities,
- "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
- "source_id": sample.sourceRevision.source.bundleIdentifier,
- "source_name": sample.sourceRevision.source.name,
- ]
- }
- DispatchQueue.main.async {
- result(dictionaries)
- }
-
- case let (nutritionSample as [HKCorrelation]) as Any:
- var foods: [[String: Any?]] = []
- for food in nutritionSample {
- let name = food.metadata?[HKMetadataKeyFoodType] as? String
- let mealType = food.metadata?["HKFoodMeal"]
- let samples = food.objects
- // get first sample if it exists
- if let sample = samples.first as? HKQuantitySample {
- var sampleDict = [
- "uuid": "\(sample.uuid)",
- "name": name,
- "meal_type": mealType,
- "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
- "source_id": sample.sourceRevision.source.bundleIdentifier,
- "source_name": sample.sourceRevision.source.name,
- "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
- ? RecordingMethod.manual.rawValue
- : RecordingMethod.automatic.rawValue
- ]
- for sample in samples {
- if let quantitySample = sample as? HKQuantitySample {
- for (key, identifier) in NUTRITION_KEYS {
- if (quantitySample.quantityType == HKObjectType.quantityType(forIdentifier: identifier)){
- let unit = key == "calories" ? HKUnit.kilocalorie() : key == "water" ? HKUnit.literUnit(with: .milli) : HKUnit.gram()
- sampleDict[key] = quantitySample.quantity.doubleValue(for: unit)
- }
- }
- }
- }
- foods.append(sampleDict)
- }
- }
-
- DispatchQueue.main.async {
- result(foods)
- }
-
- default:
- if #available(iOS 14.0, *), let ecgSamples = samplesOrNil as? [HKElectrocardiogram] {
- let dictionaries = ecgSamples.map(fetchEcgMeasurements)
- DispatchQueue.main.async {
- result(dictionaries)
- }
- } else {
- DispatchQueue.main.async {
- print("Error getting ECG - only available on iOS 14.0 and above!")
- result(nil)
- }
- }
- }
+ if #available(iOS 13.6, *) {
+ initializeIOS13_6Types()
}
- HKHealthStore().execute(query)
- }
-
- private func sanitizeMetadata(_ metadata: [String: Any]?) -> [String: Any] {
- guard let metadata = metadata else { return [:] }
-
- var sanitized = [String: Any]()
-
- for (key, value) in metadata {
- switch value {
- case let stringValue as String:
- sanitized[key] = stringValue
- case let numberValue as NSNumber:
- sanitized[key] = numberValue
- case let boolValue as Bool:
- sanitized[key] = boolValue
- case let arrayValue as [Any]:
- sanitized[key] = sanitizeArray(arrayValue)
- case let mapValue as [String: Any]:
- sanitized[key] = sanitizeMetadata(mapValue)
- default:
- continue
- }
+ if #available(iOS 14.0, *) {
+ initializeIOS14Types()
}
- return sanitized
- }
-
- private func sanitizeArray(_ array: [Any]) -> [Any] {
- var sanitizedArray: [Any] = []
-
- for value in array {
- switch value {
- case let stringValue as String:
- sanitizedArray.append(stringValue)
- case let numberValue as NSNumber:
- sanitizedArray.append(numberValue)
- case let boolValue as Bool:
- sanitizedArray.append(boolValue)
- case let arrayValue as [Any]:
- sanitizedArray.append(sanitizeArray(arrayValue))
- case let mapValue as [String: Any]:
- sanitizedArray.append(sanitizeMetadata(mapValue))
- default:
- continue
- }
+ if #available(iOS 16.0, *) {
+ initializeIOS16Types()
}
- return sanitizedArray
+ // Concatenate heart events, headache and health data types (both may be empty)
+ allDataTypes = Set(heartRateEventTypes + healthDataTypes)
+ allDataTypes = allDataTypes.union(headacheType)
}
- @available(iOS 14.0, *)
- private func fetchEcgMeasurements(_ sample: HKElectrocardiogram) -> NSDictionary {
- let semaphore = DispatchSemaphore(value: 0)
- var voltageValues = [NSDictionary]()
- let voltageQuery = HKElectrocardiogramQuery(sample) { query, result in
- switch result {
- case let .measurement(measurement):
- if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) {
- let voltage = voltageQuantity.doubleValue(for: HKUnit.volt())
- let timeSinceSampleStart = measurement.timeSinceSampleStart
- voltageValues.append(["voltage": voltage, "timeSinceSampleStart": timeSinceSampleStart])
- }
- case .done:
- semaphore.signal()
- case let .error(error):
- print(error)
- }
- }
- HKHealthStore().execute(voltageQuery)
- semaphore.wait()
- return [
- "uuid": "\(sample.uuid)",
- "voltageValues": voltageValues,
- "averageHeartRate": sample.averageHeartRate?.doubleValue(
- for: HKUnit.count().unitDivided(by: HKUnit.minute())),
- "samplingFrequency": sample.samplingFrequency?.doubleValue(for: HKUnit.hertz()),
- "classification": sample.classification.rawValue,
- "date_from": Int(sample.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000),
- "source_id": sample.sourceRevision.source.bundleIdentifier,
- "source_name": sample.sourceRevision.source.name,
- ]
+ /// Initialize iOS 11 specific data types
+ @available(iOS 11.0, *)
+ private func initializeIOS11Types() {
+ dataQuantityTypesDict[HealthConstants.ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
+ dataQuantityTypesDict[HealthConstants.BASAL_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned)!
+ dataQuantityTypesDict[HealthConstants.BLOOD_GLUCOSE] = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!
+ dataQuantityTypesDict[HealthConstants.BLOOD_OXYGEN] = HKQuantityType.quantityType(forIdentifier: .oxygenSaturation)!
+ dataQuantityTypesDict[HealthConstants.BLOOD_PRESSURE_DIASTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
+ dataQuantityTypesDict[HealthConstants.BLOOD_PRESSURE_SYSTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
+ dataQuantityTypesDict[HealthConstants.BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)!
+ dataQuantityTypesDict[HealthConstants.LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)!
+ dataQuantityTypesDict[HealthConstants.BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)!
+ dataQuantityTypesDict[HealthConstants.BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)!
+
+ // Initialize nutrition quantity types
+ initializeNutritionQuantityTypes()
+
+ dataQuantityTypesDict[HealthConstants.ELECTRODERMAL_ACTIVITY] = HKQuantityType.quantityType(forIdentifier: .electrodermalActivity)!
+ dataQuantityTypesDict[HealthConstants.FORCED_EXPIRATORY_VOLUME] = HKQuantityType.quantityType(forIdentifier: .forcedExpiratoryVolume1)!
+ dataQuantityTypesDict[HealthConstants.HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .heartRate)!
+ dataQuantityTypesDict[HealthConstants.HEART_RATE_VARIABILITY_SDNN] = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
+ dataQuantityTypesDict[HealthConstants.HEIGHT] = HKQuantityType.quantityType(forIdentifier: .height)!
+ dataQuantityTypesDict[HealthConstants.RESTING_HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .restingHeartRate)!
+ dataQuantityTypesDict[HealthConstants.STEPS] = HKQuantityType.quantityType(forIdentifier: .stepCount)!
+ dataQuantityTypesDict[HealthConstants.WAIST_CIRCUMFERENCE] = HKQuantityType.quantityType(forIdentifier: .waistCircumference)!
+ dataQuantityTypesDict[HealthConstants.WALKING_HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .walkingHeartRateAverage)!
+ dataQuantityTypesDict[HealthConstants.WEIGHT] = HKQuantityType.quantityType(forIdentifier: .bodyMass)!
+ dataQuantityTypesDict[HealthConstants.DISTANCE_WALKING_RUNNING] = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
+ dataQuantityTypesDict[HealthConstants.DISTANCE_SWIMMING] = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)!
+ dataQuantityTypesDict[HealthConstants.DISTANCE_CYCLING] = HKQuantityType.quantityType(forIdentifier: .distanceCycling)!
+ dataQuantityTypesDict[HealthConstants.FLIGHTS_CLIMBED] = HKQuantityType.quantityType(forIdentifier: .flightsClimbed)!
}
- func getIntervalData(call: FlutterMethodCall, result: @escaping FlutterResult) {
- let arguments = call.arguments as? NSDictionary
- let dataTypeKey = (arguments?["dataTypeKey"] as? String) ?? "DEFAULT"
- let dataUnitKey = (arguments?["dataUnitKey"] as? String)
- let startDate = (arguments?["startTime"] as? NSNumber) ?? 0
- let endDate = (arguments?["endTime"] as? NSNumber) ?? 0
- let intervalInSecond = (arguments?["interval"] as? Int) ?? 1
- let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? []
- let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue)
-
- // Set interval in seconds.
- var interval = DateComponents()
- interval.second = intervalInSecond
-
- // Convert dates from milliseconds to Date()
- let dateFrom = Date(timeIntervalSince1970: startDate.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endDate.doubleValue / 1000)
-
- let quantityType: HKQuantityType! = dataQuantityTypesDict[dataTypeKey]
- var predicate = HKQuery.predicateForSamples(withStart: dateFrom, end: dateTo, options: [])
- if (!includeManualEntry) {
- let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
- predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
- }
-
- let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: [.cumulativeSum, .separateBySource], anchorDate: dateFrom, intervalComponents: interval)
-
- query.initialResultsHandler = {
- [weak self] _, statisticCollectionOrNil, error in
- guard let self = self else {
- // Handle the case where self became nil.
- print("Self is nil")
- DispatchQueue.main.async {
- result(nil)
- }
- return
- }
-
- // Error detected.
- if let error = error {
- print("Query error: \(error.localizedDescription)")
- DispatchQueue.main.async {
- result(nil)
- }
- return
- }
-
- guard let collection = statisticCollectionOrNil as? HKStatisticsCollection else {
- print("Unexpected result from query")
- DispatchQueue.main.async {
- result(nil)
- }
- return
- }
-
- var dictionaries = [[String: Any]]()
- collection.enumerateStatistics(from: dateFrom, to: dateTo) {
- [weak self] statisticData, _ in
- guard let self = self else {
- // Handle the case where self became nil.
- print("Self is nil during enumeration")
- return
- }
-
- do {
- if let quantity = statisticData.sumQuantity(),
- let dataUnitKey = dataUnitKey,
- let unit = self.unitDict[dataUnitKey] {
- let dict = [
- "value": quantity.doubleValue(for: unit),
- "date_from": Int(statisticData.startDate.timeIntervalSince1970 * 1000),
- "date_to": Int(statisticData.endDate.timeIntervalSince1970 * 1000),
- "source_id": statisticData.sources?.first?.bundleIdentifier ?? "",
- "source_name": statisticData.sources?.first?.name ?? ""
- ]
- dictionaries.append(dict)
- }
- } catch {
- print("Error during collection.enumeration: \(error)")
- }
- }
- DispatchQueue.main.async {
- result(dictionaries)
- }
- }
- HKHealthStore().execute(query)
+ /// Initialize nutrition quantity types
+ @available(iOS 11.0, *)
+ private func initializeNutritionQuantityTypes() {
+ dataQuantityTypesDict[HealthConstants.DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_FIBER] = HKSampleType.quantityType(forIdentifier: .dietaryFiber)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_SUGAR] = HKSampleType.quantityType(forIdentifier: .dietarySugar)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_FAT_MONOUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatMonounsaturated)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_FAT_POLYUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_FAT_SATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatSaturated)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_CHOLESTEROL] = HKSampleType.quantityType(forIdentifier: .dietaryCholesterol)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_A] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminA)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_THIAMIN] = HKSampleType.quantityType(forIdentifier: .dietaryThiamin)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_RIBOFLAVIN] = HKSampleType.quantityType(forIdentifier: .dietaryRiboflavin)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_NIACIN] = HKSampleType.quantityType(forIdentifier: .dietaryNiacin)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_PANTOTHENIC_ACID] = HKSampleType.quantityType(forIdentifier: .dietaryPantothenicAcid)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_B6] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB6)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_BIOTIN] = HKSampleType.quantityType(forIdentifier: .dietaryBiotin)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_B12] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB12)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_C] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminC)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_D] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminD)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_E] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminE)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_VITAMIN_K] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminK)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_FOLATE] = HKSampleType.quantityType(forIdentifier: .dietaryFolate)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_CALCIUM] = HKSampleType.quantityType(forIdentifier: .dietaryCalcium)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_CHLORIDE] = HKSampleType.quantityType(forIdentifier: .dietaryChloride)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_IRON] = HKSampleType.quantityType(forIdentifier: .dietaryIron)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_MAGNESIUM] = HKSampleType.quantityType(forIdentifier: .dietaryMagnesium)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_PHOSPHORUS] = HKSampleType.quantityType(forIdentifier: .dietaryPhosphorus)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_POTASSIUM] = HKSampleType.quantityType(forIdentifier: .dietaryPotassium)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_SODIUM] = HKSampleType.quantityType(forIdentifier: .dietarySodium)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_ZINC] = HKSampleType.quantityType(forIdentifier: .dietaryZinc)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_CHROMIUM] = HKSampleType.quantityType(forIdentifier: .dietaryChromium)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_COPPER] = HKSampleType.quantityType(forIdentifier: .dietaryCopper)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_IODINE] = HKSampleType.quantityType(forIdentifier: .dietaryIodine)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)!
+ dataQuantityTypesDict[HealthConstants.DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)!
}
- func getTotalStepsInInterval(call: FlutterMethodCall, result: @escaping FlutterResult) {
- let arguments = call.arguments as? NSDictionary
- let startTime = (arguments?["startTime"] as? NSNumber) ?? 0
- let endTime = (arguments?["endTime"] as? NSNumber) ?? 0
- let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? []
- let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue)
-
- // Convert dates from milliseconds to Date()
- let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000)
- let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000)
-
- let sampleType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
- var predicate = HKQuery.predicateForSamples(
- withStart: dateFrom, end: dateTo, options: .strictStartDate)
- if (!includeManualEntry) {
- let manualPredicate = NSPredicate(format: "metadata.%K != YES", HKMetadataKeyWasUserEntered)
- predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
- }
-
- // TODO: [NOTE] Computational heavy
- let query = HKStatisticsCollectionQuery(
- quantityType: sampleType,
- quantitySamplePredicate: predicate,
- options: .cumulativeSum,
- anchorDate: dateFrom,
- intervalComponents: DateComponents(day: 1)
- )
- query.initialResultsHandler = { query, results, error in
- guard let results = results else {
- let error = error! as NSError
- print("Error getting total steps in interval \(error.localizedDescription)")
-
- DispatchQueue.main.async {
- result(nil)
- }
- return
- }
-
- var totalSteps = 0.0
- results.enumerateStatistics(from: dateFrom, to: dateTo) { statistics, stop in
- if let quantity = statistics.sumQuantity() {
- let unit = HKUnit.count()
- totalSteps += quantity.doubleValue(for: unit)
- }
- }
-
- DispatchQueue.main.async {
- result(Int(totalSteps))
- }
- }
-
- HKHealthStore().execute(query)
+ /// Initialize iOS 13 specific data types
+ @available(iOS 13.0, *)
+ private func initializeIOS13Types() {
+ dataTypesDict[HealthConstants.ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!
+ dataTypesDict[HealthConstants.AUDIOGRAM] = HKSampleType.audiogramSampleType()
+ dataTypesDict[HealthConstants.BASAL_ENERGY_BURNED] = HKSampleType.quantityType(forIdentifier: .basalEnergyBurned)!
+ dataTypesDict[HealthConstants.BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)!
+ dataTypesDict[HealthConstants.BLOOD_OXYGEN] = HKSampleType.quantityType(forIdentifier: .oxygenSaturation)!
+ dataTypesDict[HealthConstants.RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)!
+ dataTypesDict[HealthConstants.PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType(forIdentifier: .peripheralPerfusionIndex)!
+
+ dataTypesDict[HealthConstants.BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!
+ dataTypesDict[HealthConstants.BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!
+ dataTypesDict[HealthConstants.BODY_FAT_PERCENTAGE] = HKSampleType.quantityType(forIdentifier: .bodyFatPercentage)!
+ dataTypesDict[HealthConstants.LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)!
+ dataTypesDict[HealthConstants.BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)!
+ dataTypesDict[HealthConstants.BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)!
+
+ // Initialize nutrition types
+ initializeNutritionTypes()
+
+ dataTypesDict[HealthConstants.ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType(forIdentifier: .electrodermalActivity)!
+ dataTypesDict[HealthConstants.FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType(forIdentifier: .forcedExpiratoryVolume1)!
+ dataTypesDict[HealthConstants.HEART_RATE] = HKSampleType.quantityType(forIdentifier: .heartRate)!
+ dataTypesDict[HealthConstants.HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
+ dataTypesDict[HealthConstants.HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)!
+ dataTypesDict[HealthConstants.INSULIN_DELIVERY] = HKSampleType.quantityType(forIdentifier: .insulinDelivery)!
+ dataTypesDict[HealthConstants.RESTING_HEART_RATE] = HKSampleType.quantityType(forIdentifier: .restingHeartRate)!
+ dataTypesDict[HealthConstants.STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)!
+ dataTypesDict[HealthConstants.WAIST_CIRCUMFERENCE] = HKSampleType.quantityType(forIdentifier: .waistCircumference)!
+ dataTypesDict[HealthConstants.WALKING_HEART_RATE] = HKSampleType.quantityType(forIdentifier: .walkingHeartRateAverage)!
+ dataTypesDict[HealthConstants.WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)!
+ dataTypesDict[HealthConstants.DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType(forIdentifier: .distanceWalkingRunning)!
+ dataTypesDict[HealthConstants.DISTANCE_SWIMMING] = HKSampleType.quantityType(forIdentifier: .distanceSwimming)!
+ dataTypesDict[HealthConstants.DISTANCE_CYCLING] = HKSampleType.quantityType(forIdentifier: .distanceCycling)!
+ dataTypesDict[HealthConstants.FLIGHTS_CLIMBED] = HKSampleType.quantityType(forIdentifier: .flightsClimbed)!
+ dataTypesDict[HealthConstants.MINDFULNESS] = HKSampleType.categoryType(forIdentifier: .mindfulSession)!
+ dataTypesDict[HealthConstants.SLEEP_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
+ dataTypesDict[HealthConstants.SLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
+ dataTypesDict[HealthConstants.SLEEP_IN_BED] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
+ dataTypesDict[HealthConstants.SLEEP_LIGHT] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
+ dataTypesDict[HealthConstants.SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
+ dataTypesDict[HealthConstants.SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
+ dataTypesDict[HealthConstants.MENSTRUATION_FLOW] = HKSampleType.categoryType(forIdentifier: .menstrualFlow)!
+
+ dataTypesDict[HealthConstants.EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)!
+ dataTypesDict[HealthConstants.WORKOUT] = HKSampleType.workoutType()
+ dataTypesDict[HealthConstants.NUTRITION] = HKSampleType.correlationType(forIdentifier: .food)!
+
+ characteristicsTypesDict[HealthConstants.BIRTH_DATE] = HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!
+ characteristicsTypesDict[HealthConstants.GENDER] = HKObjectType.characteristicType(forIdentifier: .biologicalSex)!
+ characteristicsTypesDict[HealthConstants.BLOOD_TYPE] = HKObjectType.characteristicType(forIdentifier: .bloodType)!
}
- func unitLookUp(key: String) -> HKUnit {
- guard let unit = unitDict[key] else {
- return HKUnit.count()
- }
- return unit
+ /// Initialize nutrition types for iOS 13+
+ @available(iOS 13.0, *)
+ private func initializeNutritionTypes() {
+ dataTypesDict[HealthConstants.DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!
+ dataTypesDict[HealthConstants.DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)!
+ dataTypesDict[HealthConstants.DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
+ dataTypesDict[HealthConstants.DIETARY_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)!
+ dataTypesDict[HealthConstants.DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)!
+ dataTypesDict[HealthConstants.DIETARY_FIBER] = HKSampleType.quantityType(forIdentifier: .dietaryFiber)!
+ dataTypesDict[HealthConstants.DIETARY_SUGAR] = HKSampleType.quantityType(forIdentifier: .dietarySugar)!
+ dataTypesDict[HealthConstants.DIETARY_FAT_MONOUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatMonounsaturated)!
+ dataTypesDict[HealthConstants.DIETARY_FAT_POLYUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)!
+ dataTypesDict[HealthConstants.DIETARY_FAT_SATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatSaturated)!
+ dataTypesDict[HealthConstants.DIETARY_CHOLESTEROL] = HKSampleType.quantityType(forIdentifier: .dietaryCholesterol)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_A] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminA)!
+ dataTypesDict[HealthConstants.DIETARY_THIAMIN] = HKSampleType.quantityType(forIdentifier: .dietaryThiamin)!
+ dataTypesDict[HealthConstants.DIETARY_RIBOFLAVIN] = HKSampleType.quantityType(forIdentifier: .dietaryRiboflavin)!
+ dataTypesDict[HealthConstants.DIETARY_NIACIN] = HKSampleType.quantityType(forIdentifier: .dietaryNiacin)!
+ dataTypesDict[HealthConstants.DIETARY_PANTOTHENIC_ACID] = HKSampleType.quantityType(forIdentifier: .dietaryPantothenicAcid)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_B6] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB6)!
+ dataTypesDict[HealthConstants.DIETARY_BIOTIN] = HKSampleType.quantityType(forIdentifier: .dietaryBiotin)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_B12] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB12)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_C] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminC)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_D] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminD)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_E] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminE)!
+ dataTypesDict[HealthConstants.DIETARY_VITAMIN_K] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminK)!
+ dataTypesDict[HealthConstants.DIETARY_FOLATE] = HKSampleType.quantityType(forIdentifier: .dietaryFolate)!
+ dataTypesDict[HealthConstants.DIETARY_CALCIUM] = HKSampleType.quantityType(forIdentifier: .dietaryCalcium)!
+ dataTypesDict[HealthConstants.DIETARY_CHLORIDE] = HKSampleType.quantityType(forIdentifier: .dietaryChloride)!
+ dataTypesDict[HealthConstants.DIETARY_IRON] = HKSampleType.quantityType(forIdentifier: .dietaryIron)!
+ dataTypesDict[HealthConstants.DIETARY_MAGNESIUM] = HKSampleType.quantityType(forIdentifier: .dietaryMagnesium)!
+ dataTypesDict[HealthConstants.DIETARY_PHOSPHORUS] = HKSampleType.quantityType(forIdentifier: .dietaryPhosphorus)!
+ dataTypesDict[HealthConstants.DIETARY_POTASSIUM] = HKSampleType.quantityType(forIdentifier: .dietaryPotassium)!
+ dataTypesDict[HealthConstants.DIETARY_SODIUM] = HKSampleType.quantityType(forIdentifier: .dietarySodium)!
+ dataTypesDict[HealthConstants.DIETARY_ZINC] = HKSampleType.quantityType(forIdentifier: .dietaryZinc)!
+ dataTypesDict[HealthConstants.DIETARY_WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)!
+ dataTypesDict[HealthConstants.DIETARY_CHROMIUM] = HKSampleType.quantityType(forIdentifier: .dietaryChromium)!
+ dataTypesDict[HealthConstants.DIETARY_COPPER] = HKSampleType.quantityType(forIdentifier: .dietaryCopper)!
+ dataTypesDict[HealthConstants.DIETARY_IODINE] = HKSampleType.quantityType(forIdentifier: .dietaryIodine)!
+ dataTypesDict[HealthConstants.DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)!
+ dataTypesDict[HealthConstants.DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)!
+ dataTypesDict[HealthConstants.DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)!
}
- func dataTypeLookUp(key: String) -> HKSampleType {
- guard let dataType_ = dataTypesDict[key] else {
- return HKSampleType.quantityType(forIdentifier: .bodyMass)!
- }
- return dataType_
+ /// Initialize iOS 12 specific data types
+ @available(iOS 12.2, *)
+ private func initializeIOS12Types() {
+ dataTypesDict[HealthConstants.HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!
+ dataTypesDict[HealthConstants.LOW_HEART_RATE_EVENT] = HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!
+ dataTypesDict[HealthConstants.IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!
+
+ heartRateEventTypes = Set([
+ HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!,
+ HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!,
+ HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!,
+ ])
}
- func getGender() -> HKBiologicalSex? {
- var bioSex:HKBiologicalSex?
- do {
- bioSex = try healthStore.biologicalSex().biologicalSex
- } catch {
- bioSex = nil
- print("Error retrieving biologicalSex: \(error)")
- }
- return bioSex
+ /// Initialize iOS 13.6 specific data types
+ @available(iOS 13.6, *)
+ private func initializeIOS13_6Types() {
+ dataTypesDict[HealthConstants.HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)!
+ dataTypesDict[HealthConstants.HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)!
+ dataTypesDict[HealthConstants.HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)!
+ dataTypesDict[HealthConstants.HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)!
+ dataTypesDict[HealthConstants.HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)!
+
+ headacheType = Set([
+ HKSampleType.categoryType(forIdentifier: .headache)!
+ ])
}
- func getBirthDate() -> Date? {
- var dob:Date?
- do {
- dob = try healthStore.dateOfBirthComponents().date
- } catch {
- dob = nil
- print("Error retrieving date of birth: \(error)")
- }
- return dob
+ /// Initialize iOS 14 specific data types
+ @available(iOS 14.0, *)
+ private func initializeIOS14Types() {
+ dataTypesDict[HealthConstants.ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType()
+
+ unitDict[HealthConstants.VOLT] = HKUnit.volt()
+ unitDict[HealthConstants.INCHES_OF_MERCURY] = HKUnit.inchesOfMercury()
+
+ workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance
+ workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance
+ workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball
+ workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown
}
- func getBloodType() -> HKBloodType? {
- var bloodType:HKBloodType?
- do {
- bloodType = try healthStore.bloodType().bloodType
- } catch {
- bloodType = nil
- print("Error retrieving blood type: \(error)")
- }
- return bloodType
+ /// Initialize iOS 16 specific data types
+ @available(iOS 16.0, *)
+ private func initializeIOS16Types() {
+ dataTypesDict[HealthConstants.ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)!
+ dataTypesDict[HealthConstants.WATER_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .waterTemperature)!
+ dataTypesDict[HealthConstants.UNDERWATER_DEPTH] = HKQuantityType.quantityType(forIdentifier: .underwaterDepth)!
+ dataTypesDict[HealthConstants.UV_INDEX] = HKSampleType.quantityType(forIdentifier: .uvExposure)!
+
+ dataQuantityTypesDict[HealthConstants.UV_INDEX] = HKQuantityType.quantityType(forIdentifier: .uvExposure)!
}
- func initializeTypes() {
- // Initialize units
- unitDict[GRAM] = HKUnit.gram()
- unitDict[KILOGRAM] = HKUnit.gramUnit(with: .kilo)
- unitDict[OUNCE] = HKUnit.ounce()
- unitDict[POUND] = HKUnit.pound()
- unitDict[STONE] = HKUnit.stone()
- unitDict[METER] = HKUnit.meter()
- unitDict[INCH] = HKUnit.inch()
- unitDict[FOOT] = HKUnit.foot()
- unitDict[YARD] = HKUnit.yard()
- unitDict[MILE] = HKUnit.mile()
- unitDict[LITER] = HKUnit.liter()
- unitDict[MILLILITER] = HKUnit.literUnit(with: .milli)
- unitDict[FLUID_OUNCE_US] = HKUnit.fluidOunceUS()
- unitDict[FLUID_OUNCE_IMPERIAL] = HKUnit.fluidOunceImperial()
- unitDict[CUP_US] = HKUnit.cupUS()
- unitDict[CUP_IMPERIAL] = HKUnit.cupImperial()
- unitDict[PINT_US] = HKUnit.pintUS()
- unitDict[PINT_IMPERIAL] = HKUnit.pintImperial()
- unitDict[PASCAL] = HKUnit.pascal()
- unitDict[MILLIMETER_OF_MERCURY] = HKUnit.millimeterOfMercury()
- unitDict[CENTIMETER_OF_WATER] = HKUnit.centimeterOfWater()
- unitDict[ATMOSPHERE] = HKUnit.atmosphere()
- unitDict[DECIBEL_A_WEIGHTED_SOUND_PRESSURE_LEVEL] = HKUnit.decibelAWeightedSoundPressureLevel()
- unitDict[SECOND] = HKUnit.second()
- unitDict[MILLISECOND] = HKUnit.secondUnit(with: .milli)
- unitDict[MINUTE] = HKUnit.minute()
- unitDict[HOUR] = HKUnit.hour()
- unitDict[DAY] = HKUnit.day()
- unitDict[JOULE] = HKUnit.joule()
- unitDict[KILOCALORIE] = HKUnit.kilocalorie()
- unitDict[LARGE_CALORIE] = HKUnit.largeCalorie()
- unitDict[SMALL_CALORIE] = HKUnit.smallCalorie()
- unitDict[DEGREE_CELSIUS] = HKUnit.degreeCelsius()
- unitDict[DEGREE_FAHRENHEIT] = HKUnit.degreeFahrenheit()
- unitDict[KELVIN] = HKUnit.kelvin()
- unitDict[DECIBEL_HEARING_LEVEL] = HKUnit.decibelHearingLevel()
- unitDict[HERTZ] = HKUnit.hertz()
- unitDict[SIEMEN] = HKUnit.siemen()
- unitDict[INTERNATIONAL_UNIT] = HKUnit.internationalUnit()
- unitDict[COUNT] = HKUnit.count()
- unitDict[PERCENT] = HKUnit.percent()
- unitDict[BEATS_PER_MINUTE] = HKUnit.init(from: "count/min")
- unitDict[RESPIRATIONS_PER_MINUTE] = HKUnit.init(from: "count/min")
- unitDict[MILLIGRAM_PER_DECILITER] = HKUnit.init(from: "mg/dL")
- unitDict[UNKNOWN_UNIT] = HKUnit.init(from: "")
- unitDict[NO_UNIT] = HKUnit.init(from: "")
-
- // Initialize workout types
+ /// Initialize workout activity types
+ private func initializeWorkoutTypes() {
workoutActivityTypeMap["ARCHERY"] = .archery
workoutActivityTypeMap["BOWLING"] = .bowling
workoutActivityTypeMap["FENCING"] = .fencing
@@ -1477,410 +619,5 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
if #available(iOS 17.0, *) {
workoutActivityTypeMap["UNDERWATER_DIVING"] = .underwaterDiving
}
-
- nutritionList = [
- DIETARY_ENERGY_CONSUMED, DIETARY_CARBS_CONSUMED, DIETARY_PROTEIN_CONSUMED,
- DIETARY_FATS_CONSUMED, DIETARY_CAFFEINE, DIETARY_FIBER, DIETARY_SUGAR,
- DIETARY_FAT_MONOUNSATURATED, DIETARY_FAT_POLYUNSATURATED, DIETARY_FAT_SATURATED,
- DIETARY_CHOLESTEROL, DIETARY_VITAMIN_A, DIETARY_THIAMIN, DIETARY_RIBOFLAVIN,
- DIETARY_NIACIN, DIETARY_PANTOTHENIC_ACID, DIETARY_VITAMIN_B6, DIETARY_BIOTIN,
- DIETARY_VITAMIN_B12, DIETARY_VITAMIN_C, DIETARY_VITAMIN_D, DIETARY_VITAMIN_E,
- DIETARY_VITAMIN_K, DIETARY_FOLATE, DIETARY_CALCIUM, DIETARY_CHLORIDE,
- DIETARY_IRON, DIETARY_MAGNESIUM, DIETARY_PHOSPHORUS, DIETARY_POTASSIUM,
- DIETARY_SODIUM, DIETARY_ZINC, DIETARY_WATER, DIETARY_CHROMIUM, DIETARY_COPPER,
- DIETARY_IODINE, DIETARY_MANGANESE, DIETARY_MOLYBDENUM, DIETARY_SELENIUM,
- ]
- // Set up iOS 13 specific types (ordinary health data types)
- if #available(iOS 13.0, *) {
- dataTypesDict[ACTIVE_ENERGY_BURNED] = HKSampleType.quantityType(
- forIdentifier: .activeEnergyBurned)!
- dataTypesDict[AUDIOGRAM] = HKSampleType.audiogramSampleType()
- dataTypesDict[BASAL_ENERGY_BURNED] = HKSampleType.quantityType(
- forIdentifier: .basalEnergyBurned)!
- dataTypesDict[BLOOD_GLUCOSE] = HKSampleType.quantityType(forIdentifier: .bloodGlucose)!
- dataTypesDict[BLOOD_OXYGEN] = HKSampleType.quantityType(forIdentifier: .oxygenSaturation)!
- dataTypesDict[RESPIRATORY_RATE] = HKSampleType.quantityType(forIdentifier: .respiratoryRate)!
- dataTypesDict[PERIPHERAL_PERFUSION_INDEX] = HKSampleType.quantityType(
- forIdentifier: .peripheralPerfusionIndex)!
-
- dataTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKSampleType.quantityType(
- forIdentifier: .bloodPressureDiastolic)!
- dataTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKSampleType.quantityType(
- forIdentifier: .bloodPressureSystolic)!
- dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType(
- forIdentifier: .bodyFatPercentage)!
- dataTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)!
- dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)!
- dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)!
-
- // Nutrition
- dataTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!
- dataTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)!
- dataTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
- dataTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)!
- dataTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)!
- dataTypesDict[DIETARY_FIBER] = HKSampleType.quantityType(forIdentifier: .dietaryFiber)!
- dataTypesDict[DIETARY_SUGAR] = HKSampleType.quantityType(forIdentifier: .dietarySugar)!
- dataTypesDict[DIETARY_FAT_MONOUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatMonounsaturated)!
- dataTypesDict[DIETARY_FAT_POLYUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)!
- dataTypesDict[DIETARY_FAT_SATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatSaturated)!
- dataTypesDict[DIETARY_CHOLESTEROL] = HKSampleType.quantityType(forIdentifier: .dietaryCholesterol)!
- dataTypesDict[DIETARY_VITAMIN_A] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminA)!
- dataTypesDict[DIETARY_THIAMIN] = HKSampleType.quantityType(forIdentifier: .dietaryThiamin)!
- dataTypesDict[DIETARY_RIBOFLAVIN] = HKSampleType.quantityType(forIdentifier: .dietaryRiboflavin)!
- dataTypesDict[DIETARY_NIACIN] = HKSampleType.quantityType(forIdentifier: .dietaryNiacin)!
- dataTypesDict[DIETARY_PANTOTHENIC_ACID] = HKSampleType.quantityType(forIdentifier: .dietaryPantothenicAcid)!
- dataTypesDict[DIETARY_VITAMIN_B6] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB6)!
- dataTypesDict[DIETARY_BIOTIN] = HKSampleType.quantityType(forIdentifier: .dietaryBiotin)!
- dataTypesDict[DIETARY_VITAMIN_B12] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB12)!
- dataTypesDict[DIETARY_VITAMIN_C] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminC)!
- dataTypesDict[DIETARY_VITAMIN_D] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminD)!
- dataTypesDict[DIETARY_VITAMIN_E] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminE)!
- dataTypesDict[DIETARY_VITAMIN_K] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminK)!
- dataTypesDict[DIETARY_FOLATE] = HKSampleType.quantityType(forIdentifier: .dietaryFolate)!
- dataTypesDict[DIETARY_CALCIUM] = HKSampleType.quantityType(forIdentifier: .dietaryCalcium)!
- dataTypesDict[DIETARY_CHLORIDE] = HKSampleType.quantityType(forIdentifier: .dietaryChloride)!
- dataTypesDict[DIETARY_IRON] = HKSampleType.quantityType(forIdentifier: .dietaryIron)!
- dataTypesDict[DIETARY_MAGNESIUM] = HKSampleType.quantityType(forIdentifier: .dietaryMagnesium)!
- dataTypesDict[DIETARY_PHOSPHORUS] = HKSampleType.quantityType(forIdentifier: .dietaryPhosphorus)!
- dataTypesDict[DIETARY_POTASSIUM] = HKSampleType.quantityType(forIdentifier: .dietaryPotassium)!
- dataTypesDict[DIETARY_SODIUM] = HKSampleType.quantityType(forIdentifier: .dietarySodium)!
- dataTypesDict[DIETARY_ZINC] = HKSampleType.quantityType(forIdentifier: .dietaryZinc)!
- dataTypesDict[DIETARY_WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)!
- dataTypesDict[DIETARY_CHROMIUM] = HKSampleType.quantityType(forIdentifier: .dietaryChromium)!
- dataTypesDict[DIETARY_COPPER] = HKSampleType.quantityType(forIdentifier: .dietaryCopper)!
- dataTypesDict[DIETARY_IODINE] = HKSampleType.quantityType(forIdentifier: .dietaryIodine)!
- dataTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)!
- dataTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)!
- dataTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)!
-
- dataTypesDict[ELECTRODERMAL_ACTIVITY] = HKSampleType.quantityType(
- forIdentifier: .electrodermalActivity)!
- dataTypesDict[FORCED_EXPIRATORY_VOLUME] = HKSampleType.quantityType(
- forIdentifier: .forcedExpiratoryVolume1)!
- dataTypesDict[HEART_RATE] = HKSampleType.quantityType(forIdentifier: .heartRate)!
- dataTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKSampleType.quantityType(
- forIdentifier: .heartRateVariabilitySDNN)!
- dataTypesDict[HEIGHT] = HKSampleType.quantityType(forIdentifier: .height)!
- dataTypesDict[INSULIN_DELIVERY] = HKSampleType.quantityType(forIdentifier: .insulinDelivery)!
- dataTypesDict[RESTING_HEART_RATE] = HKSampleType.quantityType(
- forIdentifier: .restingHeartRate)!
- dataTypesDict[STEPS] = HKSampleType.quantityType(forIdentifier: .stepCount)!
- dataTypesDict[WAIST_CIRCUMFERENCE] = HKSampleType.quantityType(
- forIdentifier: .waistCircumference)!
- dataTypesDict[WALKING_HEART_RATE] = HKSampleType.quantityType(
- forIdentifier: .walkingHeartRateAverage)!
- dataTypesDict[WEIGHT] = HKSampleType.quantityType(forIdentifier: .bodyMass)!
- dataTypesDict[DISTANCE_WALKING_RUNNING] = HKSampleType.quantityType(
- forIdentifier: .distanceWalkingRunning)!
- dataTypesDict[DISTANCE_SWIMMING] = HKSampleType.quantityType(forIdentifier: .distanceSwimming)!
- dataTypesDict[DISTANCE_CYCLING] = HKSampleType.quantityType(forIdentifier: .distanceCycling)!
- dataTypesDict[FLIGHTS_CLIMBED] = HKSampleType.quantityType(forIdentifier: .flightsClimbed)!
- dataTypesDict[MINDFULNESS] = HKSampleType.categoryType(forIdentifier: .mindfulSession)!
- dataTypesDict[SLEEP_AWAKE] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
- dataTypesDict[SLEEP_DEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
- dataTypesDict[SLEEP_IN_BED] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
- dataTypesDict[SLEEP_LIGHT] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
- dataTypesDict[SLEEP_REM] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
- dataTypesDict[SLEEP_ASLEEP] = HKSampleType.categoryType(forIdentifier: .sleepAnalysis)!
- dataTypesDict[MENSTRUATION_FLOW] = HKSampleType.categoryType(forIdentifier: .menstrualFlow)!
-
-
- dataTypesDict[EXERCISE_TIME] = HKSampleType.quantityType(forIdentifier: .appleExerciseTime)!
- dataTypesDict[WORKOUT] = HKSampleType.workoutType()
- dataTypesDict[NUTRITION] = HKSampleType.correlationType(
- forIdentifier: .food)!
-
- healthDataTypes = Array(dataTypesDict.values)
-
- characteristicsTypesDict[BIRTH_DATE] = HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!
- characteristicsTypesDict[GENDER] = HKObjectType.characteristicType(forIdentifier: .biologicalSex)!
- characteristicsTypesDict[BLOOD_TYPE] = HKObjectType.characteristicType(forIdentifier: .bloodType)!
- characteristicsDataTypes = Array(characteristicsTypesDict.values)
- }
-
- // Set up iOS 11 specific types (ordinary health data quantity types)
- if #available(iOS 11.0, *) {
- dataQuantityTypesDict[ACTIVE_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
- dataQuantityTypesDict[BASAL_ENERGY_BURNED] = HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned)!
- dataQuantityTypesDict[BLOOD_GLUCOSE] = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!
- dataQuantityTypesDict[BLOOD_OXYGEN] = HKQuantityType.quantityType(forIdentifier: .oxygenSaturation)!
- dataQuantityTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
- dataQuantityTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
- dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)!
- dataQuantityTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)!
- dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)!
- dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)!
-
- // Nutrition
- dataQuantityTypesDict[DIETARY_CARBS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryCarbohydrates)!
- dataQuantityTypesDict[DIETARY_CAFFEINE] = HKSampleType.quantityType(forIdentifier: .dietaryCaffeine)!
- dataQuantityTypesDict[DIETARY_ENERGY_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryEnergyConsumed)!
- dataQuantityTypesDict[DIETARY_FATS_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryFatTotal)!
- dataQuantityTypesDict[DIETARY_PROTEIN_CONSUMED] = HKSampleType.quantityType(forIdentifier: .dietaryProtein)!
- dataQuantityTypesDict[DIETARY_FIBER] = HKSampleType.quantityType(forIdentifier: .dietaryFiber)!
- dataQuantityTypesDict[DIETARY_SUGAR] = HKSampleType.quantityType(forIdentifier: .dietarySugar)!
- dataQuantityTypesDict[DIETARY_FAT_MONOUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatMonounsaturated)!
- dataQuantityTypesDict[DIETARY_FAT_POLYUNSATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)!
- dataQuantityTypesDict[DIETARY_FAT_SATURATED] = HKSampleType.quantityType(forIdentifier: .dietaryFatSaturated)!
- dataQuantityTypesDict[DIETARY_CHOLESTEROL] = HKSampleType.quantityType(forIdentifier: .dietaryCholesterol)!
- dataQuantityTypesDict[DIETARY_VITAMIN_A] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminA)!
- dataQuantityTypesDict[DIETARY_THIAMIN] = HKSampleType.quantityType(forIdentifier: .dietaryThiamin)!
- dataQuantityTypesDict[DIETARY_RIBOFLAVIN] = HKSampleType.quantityType(forIdentifier: .dietaryRiboflavin)!
- dataQuantityTypesDict[DIETARY_NIACIN] = HKSampleType.quantityType(forIdentifier: .dietaryNiacin)!
- dataQuantityTypesDict[DIETARY_PANTOTHENIC_ACID] = HKSampleType.quantityType(forIdentifier: .dietaryPantothenicAcid)!
- dataQuantityTypesDict[DIETARY_VITAMIN_B6] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB6)!
- dataQuantityTypesDict[DIETARY_BIOTIN] = HKSampleType.quantityType(forIdentifier: .dietaryBiotin)!
- dataQuantityTypesDict[DIETARY_VITAMIN_B12] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminB12)!
- dataQuantityTypesDict[DIETARY_VITAMIN_C] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminC)!
- dataQuantityTypesDict[DIETARY_VITAMIN_D] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminD)!
- dataQuantityTypesDict[DIETARY_VITAMIN_E] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminE)!
- dataQuantityTypesDict[DIETARY_VITAMIN_K] = HKSampleType.quantityType(forIdentifier: .dietaryVitaminK)!
- dataQuantityTypesDict[DIETARY_FOLATE] = HKSampleType.quantityType(forIdentifier: .dietaryFolate)!
- dataQuantityTypesDict[DIETARY_CALCIUM] = HKSampleType.quantityType(forIdentifier: .dietaryCalcium)!
- dataQuantityTypesDict[DIETARY_CHLORIDE] = HKSampleType.quantityType(forIdentifier: .dietaryChloride)!
- dataQuantityTypesDict[DIETARY_IRON] = HKSampleType.quantityType(forIdentifier: .dietaryIron)!
- dataQuantityTypesDict[DIETARY_MAGNESIUM] = HKSampleType.quantityType(forIdentifier: .dietaryMagnesium)!
- dataQuantityTypesDict[DIETARY_PHOSPHORUS] = HKSampleType.quantityType(forIdentifier: .dietaryPhosphorus)!
- dataQuantityTypesDict[DIETARY_POTASSIUM] = HKSampleType.quantityType(forIdentifier: .dietaryPotassium)!
- dataQuantityTypesDict[DIETARY_SODIUM] = HKSampleType.quantityType(forIdentifier: .dietarySodium)!
- dataQuantityTypesDict[DIETARY_ZINC] = HKSampleType.quantityType(forIdentifier: .dietaryZinc)!
- dataQuantityTypesDict[DIETARY_WATER] = HKSampleType.quantityType(forIdentifier: .dietaryWater)!
- dataQuantityTypesDict[DIETARY_CHROMIUM] = HKSampleType.quantityType(forIdentifier: .dietaryChromium)!
- dataQuantityTypesDict[DIETARY_COPPER] = HKSampleType.quantityType(forIdentifier: .dietaryCopper)!
- dataQuantityTypesDict[DIETARY_IODINE] = HKSampleType.quantityType(forIdentifier: .dietaryIodine)!
- dataQuantityTypesDict[DIETARY_MANGANESE] = HKSampleType.quantityType(forIdentifier: .dietaryManganese)!
- dataQuantityTypesDict[DIETARY_MOLYBDENUM] = HKSampleType.quantityType(forIdentifier: .dietaryMolybdenum)!
- dataQuantityTypesDict[DIETARY_SELENIUM] = HKSampleType.quantityType(forIdentifier: .dietarySelenium)!
-
- dataQuantityTypesDict[ELECTRODERMAL_ACTIVITY] = HKQuantityType.quantityType(forIdentifier: .electrodermalActivity)!
- dataQuantityTypesDict[FORCED_EXPIRATORY_VOLUME] = HKQuantityType.quantityType(forIdentifier: .forcedExpiratoryVolume1)!
- dataQuantityTypesDict[HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .heartRate)!
- dataQuantityTypesDict[HEART_RATE_VARIABILITY_SDNN] = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
- dataQuantityTypesDict[HEIGHT] = HKQuantityType.quantityType(forIdentifier: .height)!
- dataQuantityTypesDict[RESTING_HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .restingHeartRate)!
- dataQuantityTypesDict[STEPS] = HKQuantityType.quantityType(forIdentifier: .stepCount)!
- dataQuantityTypesDict[WAIST_CIRCUMFERENCE] = HKQuantityType.quantityType(forIdentifier: .waistCircumference)!
- dataQuantityTypesDict[WALKING_HEART_RATE] = HKQuantityType.quantityType(forIdentifier: .walkingHeartRateAverage)!
- dataQuantityTypesDict[WEIGHT] = HKQuantityType.quantityType(forIdentifier: .bodyMass)!
- dataQuantityTypesDict[DISTANCE_WALKING_RUNNING] = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
- dataQuantityTypesDict[DISTANCE_SWIMMING] = HKQuantityType.quantityType(forIdentifier: .distanceSwimming)!
- dataQuantityTypesDict[DISTANCE_CYCLING] = HKQuantityType.quantityType(forIdentifier: .distanceCycling)!
- dataQuantityTypesDict[FLIGHTS_CLIMBED] = HKQuantityType.quantityType(forIdentifier: .flightsClimbed)!
-
- healthDataQuantityTypes = Array(dataQuantityTypesDict.values)
- }
-
- // Set up heart rate data types specific to the apple watch, requires iOS 12
- if #available(iOS 12.2, *) {
- dataTypesDict[HIGH_HEART_RATE_EVENT] = HKSampleType.categoryType(
- forIdentifier: .highHeartRateEvent)!
- dataTypesDict[LOW_HEART_RATE_EVENT] = HKSampleType.categoryType(
- forIdentifier: .lowHeartRateEvent)!
- dataTypesDict[IRREGULAR_HEART_RATE_EVENT] = HKSampleType.categoryType(
- forIdentifier: .irregularHeartRhythmEvent)!
-
- heartRateEventTypes = Set([
- HKSampleType.categoryType(forIdentifier: .highHeartRateEvent)!,
- HKSampleType.categoryType(forIdentifier: .lowHeartRateEvent)!,
- HKSampleType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!,
- ])
- }
-
- if #available(iOS 13.6, *) {
- dataTypesDict[HEADACHE_UNSPECIFIED] = HKSampleType.categoryType(forIdentifier: .headache)!
- dataTypesDict[HEADACHE_NOT_PRESENT] = HKSampleType.categoryType(forIdentifier: .headache)!
- dataTypesDict[HEADACHE_MILD] = HKSampleType.categoryType(forIdentifier: .headache)!
- dataTypesDict[HEADACHE_MODERATE] = HKSampleType.categoryType(forIdentifier: .headache)!
- dataTypesDict[HEADACHE_SEVERE] = HKSampleType.categoryType(forIdentifier: .headache)!
-
- headacheType = Set([
- HKSampleType.categoryType(forIdentifier: .headache)!
- ])
- }
-
- if #available(iOS 14.0, *) {
- dataTypesDict[ELECTROCARDIOGRAM] = HKSampleType.electrocardiogramType()
-
- unitDict[VOLT] = HKUnit.volt()
- unitDict[INCHES_OF_MERCURY] = HKUnit.inchesOfMercury()
-
- workoutActivityTypeMap["CARDIO_DANCE"] = HKWorkoutActivityType.cardioDance
- workoutActivityTypeMap["SOCIAL_DANCE"] = HKWorkoutActivityType.socialDance
- workoutActivityTypeMap["PICKLEBALL"] = HKWorkoutActivityType.pickleball
- workoutActivityTypeMap["COOLDOWN"] = HKWorkoutActivityType.cooldown
- }
-
- if #available(iOS 16.0, *) {
- dataTypesDict[ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)!
-
- dataTypesDict[WATER_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .waterTemperature)!
- dataTypesDict[UNDERWATER_DEPTH] = HKQuantityType.quantityType(forIdentifier: .underwaterDepth)!
-
- dataTypesDict[UV_INDEX] = HKSampleType.quantityType(forIdentifier: .uvExposure)!
- dataQuantityTypesDict[UV_INDEX] = HKQuantityType.quantityType(forIdentifier: .uvExposure)!
-
- }
-
- // Concatenate heart events, headache and health data types (both may be empty)
- allDataTypes = Set(heartRateEventTypes + healthDataTypes)
- allDataTypes = allDataTypes.union(headacheType)
}
-
- func getWorkoutType(type: HKWorkoutActivityType) -> String {
- switch type {
- case .americanFootball:
- return "americanFootball"
- case .archery:
- return "archery"
- case .australianFootball:
- return "australianFootball"
- case .badminton:
- return "badminton"
- case .baseball:
- return "baseball"
- case .basketball:
- return "basketball"
- case .bowling:
- return "bowling"
- case .boxing:
- return "boxing"
- case .climbing:
- return "climbing"
- case .cricket:
- return "cricket"
- case .crossTraining:
- return "crossTraining"
- case .curling:
- return "curling"
- case .cycling:
- return "cycling"
- case .dance:
- return "dance"
- case .danceInspiredTraining:
- return "danceInspiredTraining"
- case .elliptical:
- return "elliptical"
- case .equestrianSports:
- return "equestrianSports"
- case .fencing:
- return "fencing"
- case .fishing:
- return "fishing"
- case .functionalStrengthTraining:
- return "functionalStrengthTraining"
- case .golf:
- return "golf"
- case .gymnastics:
- return "gymnastics"
- case .handball:
- return "handball"
- case .hiking:
- return "hiking"
- case .hockey:
- return "hockey"
- case .hunting:
- return "hunting"
- case .lacrosse:
- return "lacrosse"
- case .martialArts:
- return "martialArts"
- case .mindAndBody:
- return "mindAndBody"
- case .mixedMetabolicCardioTraining:
- return "mixedMetabolicCardioTraining"
- case .paddleSports:
- return "paddleSports"
- case .play:
- return "play"
- case .preparationAndRecovery:
- return "preparationAndRecovery"
- case .racquetball:
- return "racquetball"
- case .rowing:
- return "rowing"
- case .rugby:
- return "rugby"
- case .running:
- return "running"
- case .sailing:
- return "sailing"
- case .skatingSports:
- return "skatingSports"
- case .snowSports:
- return "snowSports"
- case .soccer:
- return "soccer"
- case .softball:
- return "softball"
- case .squash:
- return "squash"
- case .stairClimbing:
- return "stairClimbing"
- case .surfingSports:
- return "surfingSports"
- case .swimming:
- return "swimming"
- case .tableTennis:
- return "tableTennis"
- case .tennis:
- return "tennis"
- case .trackAndField:
- return "trackAndField"
- case .traditionalStrengthTraining:
- return "traditionalStrengthTraining"
- case .volleyball:
- return "volleyball"
- case .walking:
- return "walking"
- case .waterFitness:
- return "waterFitness"
- case .waterPolo:
- return "waterPolo"
- case .waterSports:
- return "waterSports"
- case .wrestling:
- return "wrestling"
- case .yoga:
- return "yoga"
- case .barre:
- return "barre"
- case .coreTraining:
- return "coreTraining"
- case .crossCountrySkiing:
- return "crossCountrySkiing"
- case .downhillSkiing:
- return "downhillSkiing"
- case .flexibility:
- return "flexibility"
- case .highIntensityIntervalTraining:
- return "highIntensityIntervalTraining"
- case .jumpRope:
- return "jumpRope"
- case .kickboxing:
- return "kickboxing"
- case .pilates:
- return "pilates"
- case .snowboarding:
- return "snowboarding"
- case .stairs:
- return "stairs"
- case .stepTraining:
- return "stepTraining"
- case .wheelchairWalkPace:
- return "wheelchairWalkPace"
- case .wheelchairRunPace:
- return "wheelchairRunPace"
- case .taiChi:
- return "taiChi"
- case .mixedCardio:
- return "mixedCardio"
- case .handCycling:
- return "handCycling"
- case .underwaterDiving:
- return "underwaterDiving"
- default:
- return "other"
- }
- }
}
diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec
index f3fb27ebf..eedfe8224 100644
--- a/packages/health/ios/health.podspec
+++ b/packages/health/ios/health.podspec
@@ -3,7 +3,7 @@
#
Pod::Spec.new do |s|
s.name = 'health'
- s.version = '12.2.1'
+ s.version = '13.0.0'
s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.'
s.description = <<-DESC
Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart
index 67eb66e1f..8da99f284 100644
--- a/packages/health/lib/health.g.dart
+++ b/packages/health/lib/health.g.dart
@@ -301,6 +301,7 @@ const _$HealthWorkoutActivityTypeEnumMap = {
HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL',
HealthWorkoutActivityType.BIKING: 'BIKING',
HealthWorkoutActivityType.BOXING: 'BOXING',
+ HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE',
HealthWorkoutActivityType.CRICKET: 'CRICKET',
HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING',
HealthWorkoutActivityType.CURLING: 'CURLING',
@@ -338,7 +339,6 @@ const _$HealthWorkoutActivityTypeEnumMap = {
HealthWorkoutActivityType.YOGA: 'YOGA',
HealthWorkoutActivityType.BARRE: 'BARRE',
HealthWorkoutActivityType.BOWLING: 'BOWLING',
- HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE',
HealthWorkoutActivityType.CLIMBING: 'CLIMBING',
HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN',
HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING',
diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml
index aef5872b8..1bbd58772 100644
--- a/packages/health/pubspec.yaml
+++ b/packages/health/pubspec.yaml
@@ -1,6 +1,6 @@
name: health
description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
-version: 12.2.1
+version: 13.0.0
homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health
environment: