Skip to content

Commit

Permalink
[VIT-7375] Apple Health Kit: add support for nutritional data (#239)
Browse files Browse the repository at this point in the history
* [VIT-7375] Apple Health Kit: add support for nutritional data
  • Loading branch information
ItachiEU authored Sep 10, 2024
1 parent 3e44a45 commit 84b0f88
Show file tree
Hide file tree
Showing 10 changed files with 636 additions and 9 deletions.
2 changes: 2 additions & 0 deletions Examples/iOS/HealthKit Tab/HealthKitExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ struct HealthKitExample: View {

makePermissionRow("Blood Pressure", resources: [.vitals(.bloodPressure)], permissions: $permissions)

makePermissionRow("Meal", resources: [.meal], permissions: $permissions)

makePermissionRow("Menstrual Cycle", resources: [.menstrualCycle], permissions: $permissions)

makePermissionRow("Temperature", resources: [.vitals(.temperature)], permissions: $permissions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public enum SummaryData: Equatable, Encodable {
case sleep(SleepPatch)
case workout(WorkoutPatch)
case menstrualCycle(MenstrualCyclePatch)
case meal(MealPatch)

public var payload: Encodable {
switch self {
Expand All @@ -175,6 +176,8 @@ public enum SummaryData: Equatable, Encodable {
return patch.workouts
case let .menstrualCycle(patch):
return patch.cycles
case let .meal(patch):
return patch.meals
}
}

Expand All @@ -192,6 +195,8 @@ public enum SummaryData: Equatable, Encodable {
return patch.sleep.count
case let .menstrualCycle(patch):
return patch.cycles.count
case let .meal(patch):
return patch.dataCount()
}
}

Expand All @@ -209,6 +214,8 @@ public enum SummaryData: Equatable, Encodable {
return "workouts"
case .menstrualCycle:
return "menstrual_cycle"
case .meal:
return "meal"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public enum VitalResource: Equatable, Hashable, Codable {
case .sleep, .individual(.vo2Max), .vitals(.bloodOxygen), .vitals(.bloodPressure),
.vitals(.glucose), .vitals(.heartRateVariability),
.nutrition(.water), .nutrition(.caffeine),
.vitals(.mindfulSession), .vitals(.temperature), .vitals(.respiratoryRate):
.vitals(.mindfulSession), .vitals(.temperature), .vitals(.respiratoryRate), .meal:
return 1
case .individual(.distanceWalkingRunning), .individual(.steps), .individual(.floorsClimbed):
return 2
Expand Down Expand Up @@ -74,6 +74,8 @@ public enum VitalResource: Equatable, Hashable, Codable {
return BackfillType.temperature
case .vitals(.respiratoryRate):
return .respiratoryRate
case .meal:
return BackfillType.meal
}
}

Expand Down Expand Up @@ -168,6 +170,7 @@ public enum VitalResource: Equatable, Hashable, Codable {
case vitals(Vitals)
case individual(Individual)
case nutrition(Nutrition)
case meal

public static var all: [VitalResource] = [
.profile,
Expand All @@ -176,6 +179,7 @@ public enum VitalResource: Equatable, Hashable, Codable {
.activity,
.sleep,
.menstrualCycle,
.meal,

.vitals(.glucose),
.vitals(.bloodPressure),
Expand Down Expand Up @@ -213,6 +217,8 @@ public enum VitalResource: Equatable, Hashable, Codable {
return "sleep"
case .menstrualCycle:
return "menstrualCycle"
case .meal:
return "meal"
case let .vitals(vitals):
return vitals.logDescription
case let .individual(individual):
Expand Down
176 changes: 176 additions & 0 deletions Sources/VitalCore/Core/Client/Data Models/Patches/MealPatch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import Foundation

public struct HealthKitNutritionRawData: Equatable, Encodable {

public let sourceBundle: String

// Macros
public let energyTotal: [LocalQuantitySample]?
public let carbohydrates: [LocalQuantitySample]?
public let fiber: [LocalQuantitySample]?
public let sugar: [LocalQuantitySample]?
public let fatTotal: [LocalQuantitySample]?
public let fatMonounsaturated: [LocalQuantitySample]?
public let fatPolyunsaturated: [LocalQuantitySample]?
public let fatSaturated: [LocalQuantitySample]?
public let cholesterol: [LocalQuantitySample]?
public let protein: [LocalQuantitySample]?

// Vitamins
public let vitaminA: [LocalQuantitySample]?
public let vitaminB1: [LocalQuantitySample]?
public let riboflavin: [LocalQuantitySample]?
public let niacin: [LocalQuantitySample]?
public let pantothenicAcid: [LocalQuantitySample]?
public let vitaminB6: [LocalQuantitySample]?
public let biotin: [LocalQuantitySample]?
public let vitaminB12: [LocalQuantitySample]?
public let vitaminC: [LocalQuantitySample]?
public let vitaminD: [LocalQuantitySample]?
public let vitaminE: [LocalQuantitySample]?
public let vitaminK: [LocalQuantitySample]?
public let folicAcid: [LocalQuantitySample]?

// Minerals
public let calcium: [LocalQuantitySample]?
public let chloride: [LocalQuantitySample]?
public let iron: [LocalQuantitySample]?
public let magnesium: [LocalQuantitySample]?
public let phosphorus: [LocalQuantitySample]?
public let potassium: [LocalQuantitySample]?
public let sodium: [LocalQuantitySample]?
public let zinc: [LocalQuantitySample]?

// Ultra-trace Minerals
public let chromium: [LocalQuantitySample]?
public let copper: [LocalQuantitySample]?
public let iodine: [LocalQuantitySample]?
public let manganese: [LocalQuantitySample]?
public let molybdenum: [LocalQuantitySample]?
public let selenium: [LocalQuantitySample]?

// Hydration & Caffeine
public let water: [LocalQuantitySample]?
public let caffeine: [LocalQuantitySample]?

public func dataCount() -> Int {
let allSamples = [
energyTotal, carbohydrates, fiber, sugar, fatTotal, fatMonounsaturated,
fatPolyunsaturated, fatSaturated, cholesterol, protein, vitaminA, vitaminB1,
riboflavin, niacin, pantothenicAcid, vitaminB6, biotin, vitaminB12, vitaminC,
vitaminD, vitaminE, vitaminK, folicAcid, calcium, chloride, iron, magnesium,
phosphorus, potassium, sodium, zinc, chromium, copper, iodine, manganese,
molybdenum, selenium, water, caffeine
]

return allSamples.compactMap { $0?.count }.max() ?? 0
}

public init(
sourceBundle: String,
energyTotal: [LocalQuantitySample]? = nil,
carbohydrates: [LocalQuantitySample]? = nil,
fiber: [LocalQuantitySample]? = nil,
sugar: [LocalQuantitySample]? = nil,
fatTotal: [LocalQuantitySample]? = nil,
fatMonounsaturated: [LocalQuantitySample]? = nil,
fatPolyunsaturated: [LocalQuantitySample]? = nil,
fatSaturated: [LocalQuantitySample]? = nil,
cholesterol: [LocalQuantitySample]? = nil,
protein: [LocalQuantitySample]? = nil,
vitaminA: [LocalQuantitySample]? = nil,
vitaminB1: [LocalQuantitySample]? = nil,
riboflavin: [LocalQuantitySample]? = nil,
niacin: [LocalQuantitySample]? = nil,
pantothenicAcid: [LocalQuantitySample]? = nil,
vitaminB6: [LocalQuantitySample]? = nil,
biotin: [LocalQuantitySample]? = nil,
vitaminB12: [LocalQuantitySample]? = nil,
vitaminC: [LocalQuantitySample]? = nil,
vitaminD: [LocalQuantitySample]? = nil,
vitaminE: [LocalQuantitySample]? = nil,
vitaminK: [LocalQuantitySample]? = nil,
folicAcid: [LocalQuantitySample]? = nil,
calcium: [LocalQuantitySample]? = nil,
chloride: [LocalQuantitySample]? = nil,
iron: [LocalQuantitySample]? = nil,
magnesium: [LocalQuantitySample]? = nil,
phosphorus: [LocalQuantitySample]? = nil,
potassium: [LocalQuantitySample]? = nil,
sodium: [LocalQuantitySample]? = nil,
zinc: [LocalQuantitySample]? = nil,
chromium: [LocalQuantitySample]? = nil,
copper: [LocalQuantitySample]? = nil,
iodine: [LocalQuantitySample]? = nil,
manganese: [LocalQuantitySample]? = nil,
molybdenum: [LocalQuantitySample]? = nil,
selenium: [LocalQuantitySample]? = nil,
water: [LocalQuantitySample]? = nil,
caffeine: [LocalQuantitySample]? = nil
) {
self.sourceBundle = sourceBundle
self.energyTotal = energyTotal
self.carbohydrates = carbohydrates
self.fiber = fiber
self.sugar = sugar
self.fatTotal = fatTotal
self.fatMonounsaturated = fatMonounsaturated
self.fatPolyunsaturated = fatPolyunsaturated
self.fatSaturated = fatSaturated
self.cholesterol = cholesterol
self.protein = protein
self.vitaminA = vitaminA
self.vitaminB1 = vitaminB1
self.riboflavin = riboflavin
self.niacin = niacin
self.pantothenicAcid = pantothenicAcid
self.vitaminB6 = vitaminB6
self.biotin = biotin
self.vitaminB12 = vitaminB12
self.vitaminC = vitaminC
self.vitaminD = vitaminD
self.vitaminE = vitaminE
self.vitaminK = vitaminK
self.folicAcid = folicAcid
self.calcium = calcium
self.chloride = chloride
self.iron = iron
self.magnesium = magnesium
self.phosphorus = phosphorus
self.potassium = potassium
self.sodium = sodium
self.zinc = zinc
self.chromium = chromium
self.copper = copper
self.iodine = iodine
self.manganese = manganese
self.molybdenum = molybdenum
self.selenium = selenium
self.water = water
self.caffeine = caffeine
}
}

public struct ManualMealCreation: Equatable, Encodable{
public let healthkit: HealthKitNutritionRawData

public init(healthkit: HealthKitNutritionRawData) {
self.healthkit = healthkit
}

public func dataCount() -> Int {
return self.healthkit.dataCount()
}
}

public struct MealPatch: Equatable, Encodable {
public let meals: [ManualMealCreation]

public init(meals: [ManualMealCreation]){
self.meals = meals
}

public func dataCount() -> Int {
return self.meals.reduce(0, {result, meal in result + meal.dataCount()})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public struct LocalQuantitySample: Hashable, Encodable {
public var productType: String?
public var type: SourceType?
public var unit: Unit
public var metadata: [String: String]?

public var sourceType: SourceType {
switch type {
Expand All @@ -28,7 +29,8 @@ public struct LocalQuantitySample: Hashable, Encodable {
productType: String? = nil,
type: SourceType? = nil,
timezoneOffset: Int? = nil,
unit: Unit
unit: Unit,
metadata: [String: String]? = nil
) {
self.value = value
self.startDate = startDate
Expand All @@ -37,6 +39,7 @@ public struct LocalQuantitySample: Hashable, Encodable {
self.productType = productType
self.type = type
self.unit = unit
self.metadata = metadata
}

public init(
Expand All @@ -46,7 +49,8 @@ public struct LocalQuantitySample: Hashable, Encodable {
productType: String? = nil,
type: SourceType? = nil,
timezoneOffset: Int? = nil,
unit: Unit
unit: Unit,
metadata: [String: String]? = nil
) {
self.init(
value: value,
Expand All @@ -55,7 +59,8 @@ public struct LocalQuantitySample: Hashable, Encodable {
sourceBundle: sourceBundle,
productType: productType,
type: type,
unit: unit
unit: unit,
metadata: metadata
)
}

Expand All @@ -76,6 +81,8 @@ public struct LocalQuantitySample: Hashable, Encodable {
case minute
case degreeCelsius = "\u{00B0}C"
case stage
case mg
case ug = "\u{03BC}g"

public var description: String {
rawValue
Expand Down
4 changes: 2 additions & 2 deletions Sources/VitalCore/Core/Encodable/AnyEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import Foundation

public struct VitalAnyEncodable: Encodable {
private let encode: (Encoder) throws -> Void

public init(_ encodable: Encodable) {
self.encode = { encoder in
try encodable.encode(to: encoder)
}
}

public func encode(to encoder: Encoder) throws {
try encode(encoder)
}
Expand Down
41 changes: 41 additions & 0 deletions Sources/VitalHealthKit/HealthKit/Abstractions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,47 @@ extension VitalHealthKitStore {
case HKSampleType.quantityType(forIdentifier: .respiratoryRate)!:
return [.vitals(.respiratoryRate)]

case
HKQuantityType.quantityType(forIdentifier: .dietaryBiotin)!,
HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed)!,
HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates)!,
HKQuantityType.quantityType(forIdentifier: .dietaryFiber)!,
HKQuantityType.quantityType(forIdentifier: .dietarySugar)!,
HKQuantityType.quantityType(forIdentifier: .dietaryFatTotal)!,
HKQuantityType.quantityType(forIdentifier: .dietaryFatMonounsaturated)!,
HKQuantityType.quantityType(forIdentifier: .dietaryFatPolyunsaturated)!,
HKQuantityType.quantityType(forIdentifier: .dietaryFatSaturated)!,
HKQuantityType.quantityType(forIdentifier: .dietaryCholesterol)!,
HKQuantityType.quantityType(forIdentifier: .dietaryProtein)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminA)!,
HKQuantityType.quantityType(forIdentifier: .dietaryThiamin)!,
HKQuantityType.quantityType(forIdentifier: .dietaryRiboflavin)!,
HKQuantityType.quantityType(forIdentifier: .dietaryNiacin)!,
HKQuantityType.quantityType(forIdentifier: .dietaryPantothenicAcid)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminB6)!,
HKQuantityType.quantityType(forIdentifier: .dietaryBiotin)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminB12)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminC)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminD)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminE)!,
HKQuantityType.quantityType(forIdentifier: .dietaryVitaminK)!,
HKQuantityType.quantityType(forIdentifier: .dietaryFolate)!,
HKQuantityType.quantityType(forIdentifier: .dietaryCalcium)!,
HKQuantityType.quantityType(forIdentifier: .dietaryChloride)!,
HKQuantityType.quantityType(forIdentifier: .dietaryIron)!,
HKQuantityType.quantityType(forIdentifier: .dietaryMagnesium)!,
HKQuantityType.quantityType(forIdentifier: .dietaryPhosphorus)!,
HKQuantityType.quantityType(forIdentifier: .dietaryPotassium)!,
HKQuantityType.quantityType(forIdentifier: .dietarySodium)!,
HKQuantityType.quantityType(forIdentifier: .dietaryZinc)!,
HKQuantityType.quantityType(forIdentifier: .dietaryChromium)!,
HKQuantityType.quantityType(forIdentifier: .dietaryCopper)!,
HKQuantityType.quantityType(forIdentifier: .dietaryIodine)!,
HKQuantityType.quantityType(forIdentifier: .dietaryManganese)!,
HKQuantityType.quantityType(forIdentifier: .dietaryMolybdenum)!,
HKQuantityType.quantityType(forIdentifier: .dietarySelenium)!:
return [.meal]

default:
if #available(iOS 15.0, *) {
switch type {
Expand Down
Loading

0 comments on commit 84b0f88

Please sign in to comment.