Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VIT-7375] Apple Health Kit: add support for nutritional data #239

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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