| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- import Combine
- import CoreData
- import Foundation
- import HealthKit
- import LoopKit
- import LoopKitUI
- import Swinject
- protocol HealthKitManager {
- /// Check all needed permissions
- /// Return false if one or more permissions are deny or not choosen
- var hasGrantedFullWritePermissions: Bool { get }
- /// Check availability to save data of BG type to Health store
- func hasGlucoseWritePermission() -> Bool
- /// Requests user to give permissions on using HealthKit
- func requestPermission() async throws -> Bool
- /// Checks whether permissions are granted for Trio to write to Health
- func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool
- /// Save blood glucose to Health store
- func uploadGlucose() async
- /// Save carbs to Health store
- func uploadCarbs() async
- /// Save Insulin to Health store
- func uploadInsulin() async
- /// Delete glucose with syncID
- func deleteGlucose(syncID: String) async
- /// delete carbs with syncID
- func deleteMealData(byID id: String, sampleType: HKSampleType) async
- /// delete insulin with syncID
- func deleteInsulin(syncID: String) async
- }
- public enum AppleHealthConfig {
- // unwraped HKObjects
- static var writePermissions: Set<HKSampleType> {
- Set([healthBGObject, healthCarbObject, healthFatObject, healthProteinObject, healthInsulinObject].compactMap { $0 }) }
- // link to object in HealthKit
- static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
- static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
- static let healthFatObject = HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)
- static let healthProteinObject = HKObjectType.quantityType(forIdentifier: .dietaryProtein)
- static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
- // MetaDataKey of Trio data in HealthStore
- static let TrioInsulinType = "Trio Insulin Type"
- }
- final class BaseHealthKitManager: HealthKitManager, Injectable {
- @Injected() private var glucoseStorage: GlucoseStorage!
- @Injected() private var healthKitStore: HKHealthStore!
- @Injected() private var settingsManager: SettingsManager!
- @Injected() private var broadcaster: Broadcaster!
- @Injected() private var carbsStorage: CarbsStorage!
- @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
- @Injected() private var deviceDataManager: DeviceDataManager!
- private var backgroundContext = CoreDataStack.shared.newTaskContext()
- private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
- private var subscriptions = Set<AnyCancellable>()
- var isAvailableOnCurrentDevice: Bool {
- HKHealthStore.isHealthDataAvailable()
- }
- init(resolver: Resolver) {
- injectServices(resolver)
- coreDataPublisher =
- changedObjectsOnManagedObjectContextDidSavePublisher()
- .receive(on: DispatchQueue.global(qos: .background))
- .share()
- .eraseToAnyPublisher()
- glucoseStorage.updatePublisher
- .receive(on: DispatchQueue.global(qos: .background))
- .sink { [weak self] _ in
- guard let self = self else { return }
- Task {
- await self.uploadGlucose()
- }
- }
- .store(in: &subscriptions)
- registerHandlers()
- guard isAvailableOnCurrentDevice,
- AppleHealthConfig.healthBGObject != nil else { return }
- debug(.service, "HealthKitManager did create")
- }
- private func registerHandlers() {
- coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
- guard let self = self else { return }
- Task { [weak self] in
- guard let self = self else { return }
- await self.uploadInsulin()
- }
- }.store(in: &subscriptions)
- coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
- guard let self = self else { return }
- Task { [weak self] in
- guard let self = self else { return }
- await self.uploadCarbs()
- }
- }.store(in: &subscriptions)
- // This works only for manual Glucose
- coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
- guard let self = self else { return }
- Task { [weak self] in
- guard let self = self else { return }
- await self.uploadGlucose()
- }
- }.store(in: &subscriptions)
- }
- func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool {
- healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
- }
- var hasGrantedFullWritePermissions: Bool {
- Set(AppleHealthConfig.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
- .intersection([.sharingDenied, .notDetermined])
- .isEmpty
- }
- func hasGlucoseWritePermission() -> Bool {
- AppleHealthConfig.healthBGObject.map { checkWriteToHealthPermissions(objectTypeToHealthStore: $0) } ?? false
- }
- func requestPermission() async throws -> Bool {
- guard isAvailableOnCurrentDevice else {
- throw HKError.notAvailableOnCurrentDevice
- }
- return try await withCheckedThrowingContinuation { continuation in
- healthKitStore.requestAuthorization(
- toShare: AppleHealthConfig.writePermissions,
- read: nil
- ) { status, error in
- if let error = error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume(returning: status)
- }
- }
- }
- }
- // Glucose Upload
- func uploadGlucose() async {
- await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
- await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
- }
- func uploadGlucose(_ glucose: [BloodGlucose]) async {
- guard settingsManager.settings.useAppleHealth,
- let sampleType = AppleHealthConfig.healthBGObject,
- checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
- glucose.isNotEmpty
- else { return }
- do {
- // Create HealthKit samples from all the passed glucose values
- let glucoseSamples = glucose.compactMap { glucoseSample -> HKQuantitySample? in
- guard let glucoseValue = glucoseSample.glucose else { return nil }
- return HKQuantitySample(
- type: sampleType,
- quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucoseValue)),
- start: glucoseSample.dateString,
- end: glucoseSample.dateString,
- metadata: [
- HKMetadataKeyExternalUUID: glucoseSample.id,
- HKMetadataKeySyncIdentifier: glucoseSample.id,
- HKMetadataKeySyncVersion: 1,
- AppleHealthConfig.TrioInsulinType: deviceDataManager?.pumpManager?.status.insulinType?.title ?? ""
- ]
- )
- }
- guard glucoseSamples.isNotEmpty else {
- debug(.service, "No glucose samples available for upload.")
- return
- }
- // Attempt to save the blood glucose samples to Apple Health
- try await healthKitStore.save(glucoseSamples)
- debug(.service, "Successfully stored \(glucoseSamples.count) blood glucose samples in HealthKit.")
- // After successful upload, update the isUploadedToHealth flag in Core Data
- await updateGlucoseAsUploaded(glucose)
- } catch {
- debug(.service, "Failed to upload glucose samples to HealthKit: \(error.localizedDescription)")
- }
- }
- private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
- await backgroundContext.perform {
- let ids = glucose.map(\.id) as NSArray
- let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
- fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
- do {
- let results = try self.backgroundContext.fetch(fetchRequest)
- for result in results {
- result.isUploadedToHealth = true
- }
- guard self.backgroundContext.hasChanges else { return }
- try self.backgroundContext.save()
- } catch let error as NSError {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
- )
- }
- }
- }
- // Carbs Upload
- func uploadCarbs() async {
- await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToHealth())
- }
- func uploadCarbs(_ carbs: [CarbsEntry]) async {
- guard settingsManager.settings.useAppleHealth,
- let carbSampleType = AppleHealthConfig.healthCarbObject,
- let fatSampleType = AppleHealthConfig.healthFatObject,
- let proteinSampleType = AppleHealthConfig.healthProteinObject,
- checkWriteToHealthPermissions(objectTypeToHealthStore: carbSampleType),
- carbs.isNotEmpty
- else { return }
- do {
- var samples: [HKQuantitySample] = []
- // Create HealthKit samples for carbs, fat, and protein
- for allSamples in carbs {
- guard let id = allSamples.id else { continue }
- let fpuID = allSamples.fpuID ?? id
- let startDate = allSamples.actualDate ?? Date()
- // Carbs Sample (only if value is greater than 0)
- let carbValue = allSamples.carbs
- if carbValue > 0 {
- let carbSample = HKQuantitySample(
- type: carbSampleType,
- quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
- start: startDate,
- end: startDate,
- metadata: [
- HKMetadataKeyExternalUUID: id,
- HKMetadataKeySyncIdentifier: id,
- HKMetadataKeySyncVersion: 1
- ]
- )
- samples.append(carbSample)
- }
- // Fat Sample (only if value is greater than 0)
- if let fatValue = allSamples.fat, fatValue > 0 {
- let fatSample = HKQuantitySample(
- type: fatSampleType,
- quantity: HKQuantity(unit: .gram(), doubleValue: Double(fatValue)),
- start: startDate,
- end: startDate,
- metadata: [
- HKMetadataKeyExternalUUID: fpuID,
- HKMetadataKeySyncIdentifier: fpuID,
- HKMetadataKeySyncVersion: 1
- ]
- )
- samples.append(fatSample)
- }
- // Protein Sample (only if value is greater than 0)
- if let proteinValue = allSamples.protein, proteinValue > 0 {
- let proteinSample = HKQuantitySample(
- type: proteinSampleType,
- quantity: HKQuantity(unit: .gram(), doubleValue: Double(proteinValue)),
- start: startDate,
- end: startDate,
- metadata: [
- HKMetadataKeyExternalUUID: fpuID,
- HKMetadataKeySyncIdentifier: fpuID,
- HKMetadataKeySyncVersion: 1
- ]
- )
- samples.append(proteinSample)
- }
- }
- // Attempt to save the samples to Apple Health
- guard samples.isNotEmpty else {
- debug(.service, "No samples available for upload.")
- return
- }
- try await healthKitStore.save(samples)
- debug(.service, "Successfully stored \(samples.count) carb samples in HealthKit.")
- // After successful upload, update the isUploadedToHealth flag in Core Data
- await updateCarbsAsUploaded(carbs)
- } catch {
- debug(.service, "Failed to upload carb samples to HealthKit: \(error.localizedDescription)")
- }
- }
- private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
- await backgroundContext.perform {
- let ids = carbs.map(\.id) as NSArray
- let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
- fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
- do {
- let results = try self.backgroundContext.fetch(fetchRequest)
- for result in results {
- result.isUploadedToHealth = true
- }
- guard self.backgroundContext.hasChanges else { return }
- try self.backgroundContext.save()
- } catch let error as NSError {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
- )
- }
- }
- }
- // Insulin Upload
- func uploadInsulin() async {
- await uploadInsulin(pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth())
- }
- func uploadInsulin(_ insulinEvents: [PumpHistoryEvent]) async {
- guard settingsManager.settings.useAppleHealth,
- let sampleType = AppleHealthConfig.healthInsulinObject,
- checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
- insulinEvents.isNotEmpty else { return }
- // Fetch existing temp basal entries from Core Data for the last 24 hours
- let fetchedInsulinEntries = await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: PumpEventStored.self,
- onContext: backgroundContext,
- predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
- NSPredicate.pumpHistoryLast24h,
- NSPredicate(format: "tempBasal != nil")
- ]),
- key: "timestamp",
- ascending: true,
- batchSize: 50
- )
- var insulinSamples: [HKQuantitySample] = []
- await backgroundContext.perform {
- guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else { return }
- for event in insulinEvents {
- switch event.type {
- case .bolus:
- // For bolus events, create a HealthKit sample directly
- if let sample = self.createSample(for: event, sampleType: sampleType) {
- debug(.service, "Created HealthKit sample for bolus entry: \(sample)")
- insulinSamples.append(sample)
- }
- case .tempBasal:
- // For temp basal events, process them and adjust overlapping durations if necessary
- guard let duration = event.duration, let amount = event.amount else { continue }
- let value = (Decimal(duration) / 60.0) * amount
- let valueRounded = self.deviceDataManager?.pumpManager?
- .roundToSupportedBolusVolume(units: Double(value)) ?? Double(value)
- // Use binary search for efficient lookup of matching entry
- if let matchingIndex = self.binarySearch(entries: existingTempBasalEntries, timestamp: event.timestamp) {
- let predecessorIndex = matchingIndex - 1
- if predecessorIndex >= 0 {
- let predecessorEntry = existingTempBasalEntries[predecessorIndex]
- if let adjustedSample = self.processPredecessorEntry(
- predecessorEntry,
- nextEventTimestamp: event.timestamp,
- sampleType: sampleType
- ) {
- insulinSamples.append(adjustedSample)
- }
- }
- let newEvent = PumpHistoryEvent(
- id: event.id,
- type: .tempBasal,
- timestamp: event.timestamp,
- amount: Decimal(valueRounded),
- duration: event.duration
- )
- if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
- debug(.service, "Created HealthKit sample for initial temp basal entry: \(sample)")
- insulinSamples.append(sample)
- }
- }
- default:
- break
- }
- }
- }
- do {
- guard insulinSamples.isNotEmpty else {
- debug(.service, "No insulin samples available for upload.")
- return
- }
- try await healthKitStore.save(insulinSamples)
- debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
- await updateInsulinAsUploaded(insulinEvents)
- } catch {
- debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
- }
- }
- // Helper function to perform binary search on the sorted entries by timestamp
- private func binarySearch(entries: [PumpEventStored], timestamp: Date) -> Int? {
- var lowerBound = 0
- var upperBound = entries.count - 1
- while lowerBound <= upperBound {
- let midIndex = (lowerBound + upperBound) / 2
- guard let midTimestamp = entries[midIndex].timestamp else { return nil }
- if midTimestamp == timestamp {
- return midIndex
- } else if midTimestamp < timestamp {
- lowerBound = midIndex + 1
- } else {
- upperBound = midIndex - 1
- }
- }
- return nil
- }
- // Helper function to create a HealthKit sample from a PumpHistoryEvent
- private func createSample(
- for event: PumpHistoryEvent,
- sampleType: HKQuantityType
- ) -> HKQuantitySample? {
- // Ensure the event has a valid insulin amount
- guard let insulinValue = event.amount else { return nil }
- // Determine the insulin delivery reason based on the event type
- let deliveryReason: HKInsulinDeliveryReason
- switch event.type {
- case .bolus:
- deliveryReason = .bolus
- case .tempBasal:
- deliveryReason = .basal
- default:
- return nil
- }
- // Calculate the end date based on the event duration
- let endDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(event.duration ?? 0)))
- // Create the HealthKit quantity sample with the appropriate metadata
- let sample = HKQuantitySample(
- type: sampleType,
- quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double(insulinValue)),
- start: event.timestamp,
- end: endDate,
- metadata: [
- HKMetadataKeyExternalUUID: event.id,
- HKMetadataKeySyncIdentifier: event.id,
- HKMetadataKeySyncVersion: 1,
- HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
- AppleHealthConfig.TrioInsulinType: deviceDataManager?.pumpManager?.status.insulinType?.title ?? ""
- ]
- )
- return sample
- }
- // Helper function to process a predecessor temp basal entry and adjust overlapping durations
- private func processPredecessorEntry(
- _ predecessorEntry: PumpEventStored,
- nextEventTimestamp: Date,
- sampleType: HKQuantityType
- ) -> HKQuantitySample? {
- // Ensure the predecessor entry has the necessary data
- guard let predecessorTimestamp = predecessorEntry.timestamp,
- let predecessorEntryId = predecessorEntry.id else { return nil }
- // Calculate the original end date of the predecessor temp basal
- let predecessorDurationMinutes = predecessorEntry.tempBasal?.duration ?? 0
- let predecessorEndDate = predecessorTimestamp.addingTimeInterval(TimeInterval(predecessorDurationMinutes * 60))
- // Check if the predecessor temp basal overlaps with the next event
- if predecessorEndDate > nextEventTimestamp {
- // Adjust the end date to the start of the next event to prevent overlap
- let adjustedEndDate = nextEventTimestamp
- // Precise duration in seconds
- let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
- // Precise duration in hours
- let adjustedDurationHours = adjustedDuration / 3600
- // Calculate the insulin rate and adjusted delivered units
- let predecessorEntryRate = predecessorEntry.tempBasal?.rate?.doubleValue ?? 0
- let adjustedDeliveredUnits = adjustedDurationHours * predecessorEntryRate
- let adjustedDeliveredUnitsRounded = deviceDataManager?.pumpManager?
- .roundToSupportedBolusVolume(units: adjustedDeliveredUnits) ?? adjustedDeliveredUnits
- // Create the HealthKit quantity sample with the appropriate metadata
- // Intentionally do it here manually and do not use `createSample()` to handle utmost precise `end`.
- let sample = HKQuantitySample(
- type: sampleType,
- quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double(adjustedDeliveredUnitsRounded)),
- start: predecessorTimestamp,
- end: adjustedEndDate,
- metadata: [
- HKMetadataKeyExternalUUID: predecessorEntryId,
- HKMetadataKeySyncIdentifier: predecessorEntryId,
- HKMetadataKeySyncVersion: 2, // set the version # to 2, as we update an entry. initial version is 1.
- HKMetadataKeyInsulinDeliveryReason: HKInsulinDeliveryReason.basal.rawValue,
- AppleHealthConfig.TrioInsulinType: deviceDataManager?.pumpManager?.status.insulinType?.title ?? ""
- ]
- )
- debug(.service, "Created HealthKit sample for adjusted temp basal entry: \(sample)")
- // Create and return the HealthKit sample for the adjusted event
- return sample
- }
- // If there is no overlap, no adjustment is needed
- return nil
- }
- private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
- await backgroundContext.perform {
- let ids = insulin.map(\.id) as NSArray
- let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
- fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
- do {
- let results = try self.backgroundContext.fetch(fetchRequest)
- for result in results {
- result.isUploadedToHealth = true
- }
- guard self.backgroundContext.hasChanges else { return }
- try self.backgroundContext.save()
- } catch let error as NSError {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
- )
- }
- }
- }
- // Delete Glucose/Carbs/Insulin
- func deleteGlucose(syncID: String) async {
- guard settingsManager.settings.useAppleHealth,
- let sampleType = AppleHealthConfig.healthBGObject,
- checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType)
- else { return }
- let predicate = HKQuery.predicateForObjects(
- withMetadataKey: HKMetadataKeySyncIdentifier,
- operatorType: .equalTo,
- value: syncID
- )
- do {
- try await deleteObjects(of: sampleType, predicate: predicate)
- debug(.service, "Successfully deleted glucose sample with syncID: \(syncID)")
- } catch {
- warning(.service, "Failed to delete glucose sample with syncID: \(syncID)", error: error)
- }
- }
- func deleteMealData(byID id: String, sampleType: HKSampleType) async {
- guard settingsManager.settings.useAppleHealth else { return }
- let predicate = HKQuery.predicateForObjects(
- withMetadataKey: HKMetadataKeySyncIdentifier,
- operatorType: .equalTo,
- value: id
- )
- do {
- try await deleteObjects(of: sampleType, predicate: predicate)
- debug(.service, "Successfully deleted \(sampleType) with syncID: \(id)")
- } catch {
- warning(.service, "Failed to delete carbs sample with syncID: \(id)", error: error)
- }
- }
- func deleteInsulin(syncID: String) async {
- guard settingsManager.settings.useAppleHealth,
- let sampleType = AppleHealthConfig.healthInsulinObject,
- checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType)
- else {
- debug(.service, "HealthKit permissions are not available for insulin deletion.")
- return
- }
- let predicate = HKQuery.predicateForObjects(
- withMetadataKey: HKMetadataKeySyncIdentifier,
- operatorType: .equalTo,
- value: syncID
- )
- do {
- try await deleteObjects(of: sampleType, predicate: predicate)
- debug(.service, "Successfully deleted insulin sample with syncID: \(syncID)")
- } catch {
- warning(.service, "Failed to delete insulin sample with syncID: \(syncID)", error: error)
- }
- }
- private func deleteObjects(of sampleType: HKSampleType, predicate: NSPredicate) async throws {
- try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
- healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { success, _, error in
- if let error = error {
- continuation.resume(throwing: error)
- } else if success {
- continuation.resume(returning: ())
- }
- }
- }
- }
- }
- enum HealthKitPermissionRequestStatus {
- case needRequest
- case didRequest
- }
- enum HKError: Error {
- // HealthKit work only iPhone (not on iPad)
- case notAvailableOnCurrentDevice
- // Some data can be not available on current iOS-device
- case dataNotAvailable
- }
- private struct InsulinBolus {
- var id: String
- var amount: Decimal
- var date: Date
- }
- private struct InsulinBasal {
- var id: String
- var amount: Decimal
- var startDelivery: Date
- var endDelivery: Date
- }
|