From 3f53a335fdab65bd96a860d5a1065d4f73a79228 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:10:58 +0200 Subject: [PATCH 1/6] Refactored Swift native implementation --- packages/health/CHANGELOG.md | 4 + .../health/ios/Classes/HealthConstants.swift | 211 ++ .../ios/Classes/HealthDataOperations.swift | 279 +++ .../health/ios/Classes/HealthDataReader.swift | 550 +++++ .../health/ios/Classes/HealthDataWriter.swift | 360 +++ .../health/ios/Classes/HealthUtilities.swift | 153 ++ .../ios/Classes/SwiftHealthPlugin.swift | 2178 ++++------------- packages/health/ios/health.podspec | 2 +- packages/health/pubspec.yaml | 2 +- 9 files changed, 2017 insertions(+), 1722 deletions(-) create mode 100644 packages/health/ios/Classes/HealthConstants.swift create mode 100644 packages/health/ios/Classes/HealthDataOperations.swift create mode 100644 packages/health/ios/Classes/HealthDataReader.swift create mode 100644 packages/health/ios/Classes/HealthDataWriter.swift create mode 100644 packages/health/ios/Classes/HealthUtilities.swift diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index fafe282ee..c81bac2dd 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,7 @@ +## 13.0.0 + +* Refactored Swift native implementation + ## 12.1.0 * Add delete record by UUID method. See function `deleteByUUID(required String uuid, HealthDataType? type)` 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..879417290 --- /dev/null +++ b/packages/health/ios/Classes/HealthDataOperations.swift @@ -0,0 +1,279 @@ +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 { + guard let dataType = dataTypesDict[key] else { + print("Warning: Health data type '\(key)' not found in dataTypesDict") + continue + } + + 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 + } + } + } + } + + 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 + } + + 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 { + // TODO: Add proper error handling + DispatchQueue.main.async { + result(false) + } + 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 8b4fa1bbf..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,1170 +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, - "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 @@ -1476,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 aba1806e8..6bd31c1cf 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.1.0' + 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/pubspec.yaml b/packages/health/pubspec.yaml index a92b82d90..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.1.0 +version: 13.0.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: From ac178f6f81d1190fabc0ea324e3c2bfedcb72aa8 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Sun, 20 Apr 2025 20:41:26 +0200 Subject: [PATCH 2/6] crp_background_location updates --- .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- packages/carp_background_location/example/ios/Podfile | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 8 ++++---- .../xcshareddata/xcschemes/Runner.xcscheme | 3 ++- .../example/ios/Runner/AppDelegate.swift | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) 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, From 246e5b4db278697e3e3d5faa3cc0ef0abaf0d73a Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Thu, 8 May 2025 11:53:08 +0200 Subject: [PATCH 3/6] auto: dart run build generated file --- packages/health/lib/health.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', From 947c702939fb6eb363fcb97036d8ba86342fe0f7 Mon Sep 17 00:00:00 2001 From: bardram Date: Sat, 17 May 2025 20:59:58 +0200 Subject: [PATCH 4/6] Small update to documentation --- .../example/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- packages/health/example/lib/util.dart | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index f9034579c..8c8e29ab8 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -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/lib/util.dart b/packages/health/example/lib/util.dart index d896c6b02..1cdf17de2 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -1,6 +1,6 @@ import 'package:health/health.dart'; -/// List of data types available on iOS +/// Data types available on iOS via Apple Health. const List 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, From 658204ef46dfdf9b7281b7fa1d70d74d11961a86 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:55:23 +0200 Subject: [PATCH 5/6] sync updates --- packages/health/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 7476dab9e..d9ef81651 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -2,6 +2,10 @@ * 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) + ## 12.2.0 * iOS: Add `deviceModel` in returned Health data to identify the device that generated the data of the receiver. (in iOS `source_name` represents the revision of the source responsible for saving the receiver.) From 4ba4ef17c11fca50d092963f2cdf875f014650c5 Mon Sep 17 00:00:00 2001 From: Alireza Hajebrahimi <6937697+iarata@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:15:44 +0200 Subject: [PATCH 6/6] Improve data type handling and error messages --- .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../ios/Classes/HealthDataOperations.swift | 54 +++++++++++++------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj index 8c8e29ab8..b856ee37b 100644 --- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 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"> () var typesToWrite = Set() + for (index, key) in types.enumerated() { if (key == HealthConstants.NUTRITION) { for nutritionType in nutritionList { @@ -125,32 +126,35 @@ class HealthDataOperations { typesToWrite.insert(nutritionData) } } else { - guard let dataType = dataTypesDict[key] else { - print("Warning: Health data type '\(key)' not found in dataTypesDict") - continue - } - 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 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] { - 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)") + throw PluginError(message: "Cannot request write permission for characteristic type \(characteristicsType)") default: - break + typesToRead.insert(characteristicsType) } } + + if dataTypesDict[key] == nil && characteristicsTypesDict[key] == nil { + print("Warning: Health data type '\(key)' not found in dataTypesDict or characteristicsTypesDict") + } + } } @@ -179,6 +183,13 @@ class HealthDataOperations { 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 @@ -205,13 +216,22 @@ class HealthDataOperations { guard let self = self else { return } guard let samplesOrNil = samplesOrNil, error == nil else { - // TODO: Add proper error handling + 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 {