HealthKitManager.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import HealthKit
  5. import LoopKit
  6. import LoopKitUI
  7. import Swinject
  8. protocol HealthKitManager {
  9. /// Check all needed permissions
  10. /// Return false if one or more permissions are deny or not choosen
  11. var hasGrantedFullWritePermissions: Bool { get }
  12. /// Check availability to save data of BG type to Health store
  13. func hasGlucoseWritePermission() -> Bool
  14. /// Requests user to give permissions on using HealthKit
  15. func requestPermission() async throws -> Bool
  16. /// Checks whether permissions are granted for Trio to write to Health
  17. func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool
  18. /// Save blood glucose to Health store
  19. func uploadGlucose() async
  20. /// Save carbs to Health store
  21. func uploadCarbs() async
  22. /// Save Insulin to Health store
  23. func uploadInsulin() async
  24. /// Delete glucose with syncID
  25. func deleteGlucose(syncID: String) async
  26. /// delete carbs with syncID
  27. func deleteMealData(byID id: String, sampleType: HKSampleType) async
  28. /// delete insulin with syncID
  29. func deleteInsulin(syncID: String) async
  30. }
  31. public enum AppleHealthConfig {
  32. // unwraped HKObjects
  33. static var writePermissions: Set<HKSampleType> {
  34. Set([healthBGObject, healthCarbObject, healthFatObject, healthProteinObject, healthInsulinObject].compactMap { $0 }) }
  35. // link to object in HealthKit
  36. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  37. static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
  38. static let healthFatObject = HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)
  39. static let healthProteinObject = HKObjectType.quantityType(forIdentifier: .dietaryProtein)
  40. static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
  41. // MetaDataKey of Trio data in HealthStore
  42. static let TrioMetaDataKey = "TrioMetaDataKey"
  43. }
  44. final class BaseHealthKitManager: HealthKitManager, Injectable {
  45. @Injected() private var glucoseStorage: GlucoseStorage!
  46. @Injected() private var healthKitStore: HKHealthStore!
  47. @Injected() private var settingsManager: SettingsManager!
  48. @Injected() private var broadcaster: Broadcaster!
  49. @Injected() var carbsStorage: CarbsStorage!
  50. @Injected() var pumpHistoryStorage: PumpHistoryStorage!
  51. private var backgroundContext = CoreDataStack.shared.newTaskContext()
  52. private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
  53. private var subscriptions = Set<AnyCancellable>()
  54. var isAvailableOnCurrentDevice: Bool {
  55. HKHealthStore.isHealthDataAvailable()
  56. }
  57. init(resolver: Resolver) {
  58. injectServices(resolver)
  59. coreDataPublisher =
  60. changedObjectsOnManagedObjectContextDidSavePublisher()
  61. .receive(on: DispatchQueue.global(qos: .background))
  62. .share()
  63. .eraseToAnyPublisher()
  64. registerHandlers()
  65. guard isAvailableOnCurrentDevice,
  66. AppleHealthConfig.healthBGObject != nil else { return }
  67. debug(.service, "HealthKitManager did create")
  68. }
  69. private func registerHandlers() {
  70. coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
  71. guard let self = self else { return }
  72. Task { [weak self] in
  73. guard let self = self else { return }
  74. await self.uploadInsulin()
  75. }
  76. }.store(in: &subscriptions)
  77. coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
  78. guard let self = self else { return }
  79. Task { [weak self] in
  80. guard let self = self else { return }
  81. await self.uploadCarbs()
  82. }
  83. }.store(in: &subscriptions)
  84. }
  85. func checkWriteToHealthPermissions(objectTypeToHealthStore: HKObjectType) -> Bool {
  86. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  87. }
  88. var hasGrantedFullWritePermissions: Bool {
  89. Set(AppleHealthConfig.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
  90. .intersection([.sharingDenied, .notDetermined])
  91. .isEmpty
  92. }
  93. func hasGlucoseWritePermission() -> Bool {
  94. AppleHealthConfig.healthBGObject.map { checkWriteToHealthPermissions(objectTypeToHealthStore: $0) } ?? false
  95. }
  96. func requestPermission() async throws -> Bool {
  97. guard isAvailableOnCurrentDevice else {
  98. throw HKError.notAvailableOnCurrentDevice
  99. }
  100. return try await withCheckedThrowingContinuation { continuation in
  101. healthKitStore.requestAuthorization(
  102. toShare: AppleHealthConfig.writePermissions,
  103. read: nil
  104. ) { status, error in
  105. if let error = error {
  106. continuation.resume(throwing: error)
  107. } else {
  108. continuation.resume(returning: status)
  109. }
  110. }
  111. }
  112. }
  113. // Glucose Upload
  114. func uploadGlucose() async {
  115. await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
  116. await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
  117. }
  118. func uploadGlucose(_ glucose: [BloodGlucose]) async {
  119. guard settingsManager.settings.useAppleHealth,
  120. let sampleType = AppleHealthConfig.healthBGObject,
  121. checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
  122. glucose.isNotEmpty
  123. else { return }
  124. do {
  125. // Create HealthKit samples from all the passed glucose values
  126. let glucoseSamples = glucose.compactMap { glucoseSample -> HKQuantitySample? in
  127. guard let glucoseValue = glucoseSample.glucose else { return nil }
  128. return HKQuantitySample(
  129. type: sampleType,
  130. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucoseValue)),
  131. start: glucoseSample.dateString,
  132. end: glucoseSample.dateString,
  133. metadata: [
  134. HKMetadataKeyExternalUUID: glucoseSample.id,
  135. HKMetadataKeySyncIdentifier: glucoseSample.id,
  136. HKMetadataKeySyncVersion: 1,
  137. AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
  138. ]
  139. )
  140. }
  141. guard glucoseSamples.isNotEmpty else {
  142. debug(.service, "No glucose samples available for upload.")
  143. return
  144. }
  145. // Attempt to save the blood glucose samples to Apple Health
  146. try await healthKitStore.save(glucoseSamples)
  147. debug(.service, "Successfully stored \(glucoseSamples.count) blood glucose samples in HealthKit.")
  148. // After successful upload, update the isUploadedToHealth flag in Core Data
  149. await updateGlucoseAsUploaded(glucose)
  150. } catch {
  151. debug(.service, "Failed to upload glucose samples to HealthKit: \(error.localizedDescription)")
  152. }
  153. }
  154. private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
  155. await backgroundContext.perform {
  156. let ids = glucose.map(\.id) as NSArray
  157. let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
  158. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  159. do {
  160. let results = try self.backgroundContext.fetch(fetchRequest)
  161. for result in results {
  162. result.isUploadedToHealth = true
  163. }
  164. guard self.backgroundContext.hasChanges else { return }
  165. try self.backgroundContext.save()
  166. } catch let error as NSError {
  167. debugPrint(
  168. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  169. )
  170. }
  171. }
  172. }
  173. // Carbs Upload
  174. func uploadCarbs() async {
  175. await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToHealth())
  176. }
  177. func uploadCarbs(_ carbs: [CarbsEntry]) async {
  178. guard settingsManager.settings.useAppleHealth,
  179. let carbSampleType = AppleHealthConfig.healthCarbObject,
  180. let fatSampleType = AppleHealthConfig.healthFatObject,
  181. let proteinSampleType = AppleHealthConfig.healthProteinObject,
  182. checkWriteToHealthPermissions(objectTypeToHealthStore: carbSampleType),
  183. carbs.isNotEmpty
  184. else { return }
  185. do {
  186. var samples: [HKQuantitySample] = []
  187. // Create HealthKit samples for carbs, fat, and protein
  188. for allSamples in carbs {
  189. guard let id = allSamples.id else { continue }
  190. let fpuID = allSamples.fpuID ?? id
  191. let startDate = allSamples.actualDate ?? Date()
  192. // Carbs Sample
  193. let carbValue = allSamples.carbs
  194. let carbSample = HKQuantitySample(
  195. type: carbSampleType,
  196. quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
  197. start: startDate,
  198. end: startDate,
  199. metadata: [
  200. HKMetadataKeyExternalUUID: id,
  201. HKMetadataKeySyncIdentifier: id,
  202. HKMetadataKeySyncVersion: 1,
  203. AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
  204. ]
  205. )
  206. samples.append(carbSample)
  207. // Fat Sample (if available)
  208. if let fatValue = allSamples.fat {
  209. let fatSample = HKQuantitySample(
  210. type: fatSampleType,
  211. quantity: HKQuantity(unit: .gram(), doubleValue: Double(fatValue)),
  212. start: startDate,
  213. end: startDate,
  214. metadata: [
  215. HKMetadataKeyExternalUUID: fpuID,
  216. HKMetadataKeySyncIdentifier: fpuID,
  217. HKMetadataKeySyncVersion: 1,
  218. AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
  219. ]
  220. )
  221. samples.append(fatSample)
  222. }
  223. // Protein Sample (if available)
  224. if let proteinValue = allSamples.protein {
  225. let proteinSample = HKQuantitySample(
  226. type: proteinSampleType,
  227. quantity: HKQuantity(unit: .gram(), doubleValue: Double(proteinValue)),
  228. start: startDate,
  229. end: startDate,
  230. metadata: [
  231. HKMetadataKeyExternalUUID: fpuID,
  232. HKMetadataKeySyncIdentifier: fpuID,
  233. HKMetadataKeySyncVersion: 1,
  234. AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
  235. ]
  236. )
  237. samples.append(proteinSample)
  238. }
  239. }
  240. // Attempt to save the samples to Apple Health
  241. guard samples.isNotEmpty else {
  242. debug(.service, "No samples available for upload.")
  243. return
  244. }
  245. try await healthKitStore.save(samples)
  246. debug(.service, "Successfully stored \(samples.count) carb samples in HealthKit.")
  247. // After successful upload, update the isUploadedToHealth flag in Core Data
  248. await updateCarbsAsUploaded(carbs)
  249. } catch {
  250. debug(.service, "Failed to upload carb samples to HealthKit: \(error.localizedDescription)")
  251. }
  252. }
  253. private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
  254. await backgroundContext.perform {
  255. let ids = carbs.map(\.id) as NSArray
  256. let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
  257. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  258. do {
  259. let results = try self.backgroundContext.fetch(fetchRequest)
  260. for result in results {
  261. result.isUploadedToHealth = true
  262. }
  263. guard self.backgroundContext.hasChanges else { return }
  264. try self.backgroundContext.save()
  265. } catch let error as NSError {
  266. debugPrint(
  267. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  268. )
  269. }
  270. }
  271. }
  272. // Insulin Upload
  273. func uploadInsulin() async {
  274. await uploadInsulin(pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth())
  275. }
  276. func uploadInsulin(_ insulin: [PumpHistoryEvent]) async {
  277. guard settingsManager.settings.useAppleHealth,
  278. let sampleType = AppleHealthConfig.healthInsulinObject,
  279. checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
  280. insulin.isNotEmpty
  281. else { return }
  282. // Fetch existing temp basal entries from Core Data for the last 24 hours
  283. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  284. ofType: PumpEventStored.self,
  285. onContext: backgroundContext,
  286. predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
  287. NSPredicate.pumpHistoryLast24h,
  288. NSPredicate(format: "tempBasal != nil")
  289. ]),
  290. key: "timestamp",
  291. ascending: true,
  292. batchSize: 50
  293. )
  294. // Initialize an array to hold the HealthKit samples to be uploaded
  295. var insulinSamples: [HKQuantitySample] = []
  296. // Perform the data processing on the background context
  297. await backgroundContext.perform {
  298. // Ensure that the fetched results are of the expected type
  299. guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
  300. // Create a mapping from timestamps to indices for quick access to existing entries
  301. let existingEntriesByTimestamp = Dictionary(
  302. uniqueKeysWithValues: existingTempBasalEntries.enumerated()
  303. .map { ($0.element.timestamp, $0.offset) }
  304. )
  305. for event in insulin {
  306. switch event.type {
  307. case .bolus:
  308. // For bolus events, create a HealthKit sample directly
  309. if let sample = self.createSample(for: event, sampleType: sampleType) {
  310. insulinSamples.append(sample)
  311. }
  312. case .tempBasal:
  313. // For temp basal events, process them and adjust overlapping durations if necessary
  314. guard let duration = event.duration, let amount = event.amount else { continue }
  315. // Calculate the total insulin delivered during the temp basal period
  316. let value = (Decimal(duration) / 60.0) * amount
  317. // Check if there's a matching existing temp basal entry
  318. if let matchingEntryIndex = existingEntriesByTimestamp[event.timestamp] {
  319. let predecessorIndex = matchingEntryIndex - 1
  320. if predecessorIndex >= 0 {
  321. // Get the predecessor entry to handle overlapping temp basal events
  322. let predecessorEntry = existingTempBasalEntries[predecessorIndex]
  323. // Adjust the predecessor entry if it overlaps with the current event
  324. if let adjustedSample = self.processPredecessorEntry(
  325. predecessorEntry,
  326. nextEventTimestamp: event.timestamp,
  327. sampleType: sampleType
  328. ) {
  329. insulinSamples.append(adjustedSample)
  330. }
  331. }
  332. // Create a new PumpHistoryEvent with the calculated insulin value
  333. let newEvent = PumpHistoryEvent(
  334. id: event.id,
  335. type: .tempBasal,
  336. timestamp: event.timestamp,
  337. amount: value,
  338. duration: event.duration
  339. )
  340. // Create a HealthKit sample for the current temp basal event
  341. if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
  342. insulinSamples.append(sample)
  343. }
  344. }
  345. default:
  346. // Ignore other event types
  347. break
  348. }
  349. }
  350. }
  351. // Save the processed insulin samples to HealthKit
  352. do {
  353. guard insulinSamples.isNotEmpty else {
  354. debug(.service, "No insulin samples available for upload.")
  355. return
  356. }
  357. // Attempt to save the samples to HealthKit
  358. try await healthKitStore.save(insulinSamples)
  359. debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
  360. // Mark the insulin events as uploaded
  361. await updateInsulinAsUploaded(insulin)
  362. } catch {
  363. debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
  364. }
  365. }
  366. // Helper function to create a HealthKit sample from a PumpHistoryEvent
  367. private func createSample(for event: PumpHistoryEvent, sampleType: HKQuantityType) -> HKQuantitySample? {
  368. // Ensure the event has a valid insulin amount
  369. guard let insulinValue = event.amount else { return nil }
  370. // Determine the insulin delivery reason based on the event type
  371. let deliveryReason: HKInsulinDeliveryReason
  372. switch event.type {
  373. case .bolus:
  374. deliveryReason = .bolus
  375. case .tempBasal:
  376. deliveryReason = .basal
  377. default:
  378. return nil
  379. }
  380. // Calculate the end date based on the event duration
  381. let endDate = event.timestamp.addingTimeInterval(TimeInterval(minutes: Double(event.duration ?? 0)))
  382. // Create the HealthKit quantity sample with the appropriate metadata
  383. let sample = HKQuantitySample(
  384. type: sampleType,
  385. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double(insulinValue)),
  386. start: event.timestamp,
  387. end: endDate,
  388. metadata: [
  389. HKMetadataKeyExternalUUID: event.id,
  390. HKMetadataKeySyncIdentifier: event.id,
  391. HKMetadataKeySyncVersion: 1,
  392. HKMetadataKeyInsulinDeliveryReason: deliveryReason.rawValue,
  393. AppleHealthConfig.TrioMetaDataKey: UUID().uuidString
  394. ]
  395. )
  396. return sample
  397. }
  398. // Helper function to process a predecessor temp basal entry and adjust overlapping durations
  399. private func processPredecessorEntry(
  400. _ predecessorEntry: PumpEventStored,
  401. nextEventTimestamp: Date,
  402. sampleType: HKQuantityType
  403. ) -> HKQuantitySample? {
  404. // Ensure the predecessor entry has the necessary data
  405. guard let predecessorTimestamp = predecessorEntry.timestamp,
  406. let predecessorEntryId = predecessorEntry.id else { return nil }
  407. // Calculate the original end date of the predecessor temp basal
  408. let predecessorDurationMinutes = predecessorEntry.tempBasal?.duration ?? 0
  409. let predecessorEndDate = predecessorTimestamp.addingTimeInterval(TimeInterval(Int(predecessorDurationMinutes) * 60))
  410. // Check if the predecessor temp basal overlaps with the next event
  411. if predecessorEndDate > nextEventTimestamp {
  412. // Adjust the end date to the start of the next event to prevent overlap
  413. let adjustedEndDate = nextEventTimestamp
  414. let adjustedDuration = adjustedEndDate.timeIntervalSince(predecessorTimestamp)
  415. // Calculate the insulin rate and adjusted delivered units
  416. let predecessorEntryRate = predecessorEntry.tempBasal?.rate?.decimalValue ?? 0
  417. let adjustedDurationHours = Decimal(adjustedDuration) / 3600
  418. let adjustedDeliveredUnits = adjustedDurationHours * predecessorEntryRate
  419. // Create a new PumpHistoryEvent with the adjusted values
  420. let adjustedEvent = PumpHistoryEvent(
  421. id: predecessorEntryId,
  422. type: .tempBasal,
  423. timestamp: predecessorTimestamp,
  424. amount: adjustedDeliveredUnits,
  425. duration: Int(adjustedDuration / 60)
  426. )
  427. // Create and return the HealthKit sample for the adjusted event
  428. return createSample(for: adjustedEvent, sampleType: sampleType)
  429. }
  430. // If there is no overlap, no adjustment is needed
  431. return nil
  432. }
  433. private func updateInsulinAsUploaded(_ insulin: [PumpHistoryEvent]) async {
  434. await backgroundContext.perform {
  435. let ids = insulin.map(\.id) as NSArray
  436. let fetchRequest: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
  437. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  438. do {
  439. let results = try self.backgroundContext.fetch(fetchRequest)
  440. for result in results {
  441. result.isUploadedToHealth = true
  442. }
  443. guard self.backgroundContext.hasChanges else { return }
  444. try self.backgroundContext.save()
  445. } catch let error as NSError {
  446. debugPrint(
  447. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  448. )
  449. }
  450. }
  451. }
  452. // Delete Glucose/Carbs/Insulin
  453. func deleteGlucose(syncID: String) async {
  454. guard settingsManager.settings.useAppleHealth,
  455. let sampleType = AppleHealthConfig.healthBGObject,
  456. checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType)
  457. else { return }
  458. let predicate = HKQuery.predicateForObjects(
  459. withMetadataKey: HKMetadataKeySyncIdentifier,
  460. operatorType: .equalTo,
  461. value: syncID
  462. )
  463. do {
  464. try await deleteObjects(of: sampleType, predicate: predicate)
  465. debug(.service, "Successfully deleted glucose sample with syncID: \(syncID)")
  466. } catch {
  467. warning(.service, "Failed to delete glucose sample with syncID: \(syncID)", error: error)
  468. }
  469. }
  470. func deleteMealData(byID id: String, sampleType: HKSampleType) async {
  471. guard settingsManager.settings.useAppleHealth else { return }
  472. let predicate = HKQuery.predicateForObjects(
  473. withMetadataKey: HKMetadataKeySyncIdentifier,
  474. operatorType: .equalTo,
  475. value: id
  476. )
  477. do {
  478. try await deleteObjects(of: sampleType, predicate: predicate)
  479. debug(.service, "Successfully deleted \(sampleType) with syncID: \(id)")
  480. } catch {
  481. warning(.service, "Failed to delete carbs sample with syncID: \(id)", error: error)
  482. }
  483. }
  484. func deleteInsulin(syncID: String) async {
  485. guard settingsManager.settings.useAppleHealth,
  486. let sampleType = AppleHealthConfig.healthInsulinObject,
  487. checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType)
  488. else {
  489. debug(.service, "HealthKit permissions are not available for insulin deletion.")
  490. return
  491. }
  492. let predicate = HKQuery.predicateForObjects(
  493. withMetadataKey: HKMetadataKeySyncIdentifier,
  494. operatorType: .equalTo,
  495. value: syncID
  496. )
  497. do {
  498. try await deleteObjects(of: sampleType, predicate: predicate)
  499. debug(.service, "Successfully deleted insulin sample with syncID: \(syncID)")
  500. } catch {
  501. warning(.service, "Failed to delete insulin sample with syncID: \(syncID)", error: error)
  502. }
  503. }
  504. private func deleteObjects(of sampleType: HKSampleType, predicate: NSPredicate) async throws {
  505. try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
  506. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { success, _, error in
  507. if let error = error {
  508. continuation.resume(throwing: error)
  509. } else if success {
  510. continuation.resume(returning: ())
  511. }
  512. }
  513. }
  514. }
  515. }
  516. enum HealthKitPermissionRequestStatus {
  517. case needRequest
  518. case didRequest
  519. }
  520. enum HKError: Error {
  521. // HealthKit work only iPhone (not on iPad)
  522. case notAvailableOnCurrentDevice
  523. // Some data can be not available on current iOS-device
  524. case dataNotAvailable
  525. }
  526. private struct InsulinBolus {
  527. var id: String
  528. var amount: Decimal
  529. var date: Date
  530. }
  531. private struct InsulinBasal {
  532. var id: String
  533. var amount: Decimal
  534. var startDelivery: Date
  535. var endDelivery: Date
  536. }