HealthKitManager.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. import Combine
  2. import Foundation
  3. import HealthKit
  4. import LoopKit
  5. import LoopKitUI
  6. import Swinject
  7. protocol HealthKitManager: GlucoseSource {
  8. /// Check all needed permissions
  9. /// Return false if one or more permissions are deny or not choosen
  10. var areAllowAllPermissions: Bool { get }
  11. /// Check availability to save data of BG type to Health store
  12. func checkAvailabilitySaveBG() -> Bool
  13. /// Requests user to give permissions on using HealthKit
  14. func requestPermission(completion: ((Bool, Error?) -> Void)?)
  15. /// Save blood glucose to Health store (dublicate of bg will ignore)
  16. func saveIfNeeded(bloodGlucose: [BloodGlucose])
  17. /// Save carbs to Health store (dublicate of bg will ignore)
  18. func saveIfNeeded(carbs: [CarbsEntry])
  19. /// Save Insulin to Health store
  20. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent])
  21. /// Create observer for data passing beetwen Health Store and FreeAPS
  22. func createBGObserver()
  23. /// Enable background delivering objects from Apple Health to FreeAPS
  24. func enableBackgroundDelivery()
  25. /// Delete glucose with syncID
  26. func deleteGlucose(syncID: String)
  27. /// delete carbs with syncID
  28. func deleteCarbs(syncID: String, isFPU: Bool?, fpuID: String?)
  29. /// delete insulin with syncID
  30. func deleteInsulin(syncID: String)
  31. }
  32. final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, PumpHistoryObserver {
  33. private enum Config {
  34. // unwraped HKObjects
  35. static var readPermissions: Set<HKSampleType> {
  36. Set([healthBGObject].compactMap { $0 }) }
  37. static var writePermissions: Set<HKSampleType> {
  38. Set([healthBGObject, healthCarbObject, healthInsulinObject].compactMap { $0 }) }
  39. // link to object in HealthKit
  40. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  41. static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
  42. static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
  43. // Meta-data key of FreeASPX data in HealthStore
  44. static let freeAPSMetaKey = "fromFreeAPSX"
  45. }
  46. @Injected() private var glucoseStorage: GlucoseStorage!
  47. @Injected() private var healthKitStore: HKHealthStore!
  48. @Injected() private var settingsManager: SettingsManager!
  49. @Injected() private var broadcaster: Broadcaster!
  50. @Injected() var carbsStorage: CarbsStorage!
  51. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  52. private var lifetime = Lifetime()
  53. // BG that will be return Publisher
  54. @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
  55. // last anchor for HKAnchoredQuery
  56. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
  57. set {
  58. persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  59. }
  60. get {
  61. guard let data = persistedBGAnchor else { return nil }
  62. return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
  63. }
  64. }
  65. @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil
  66. var isAvailableOnCurrentDevice: Bool {
  67. HKHealthStore.isHealthDataAvailable()
  68. }
  69. var areAllowAllPermissions: Bool {
  70. Set(Config.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
  71. .intersection([.notDetermined])
  72. .isEmpty &&
  73. Set(Config.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
  74. .intersection([.sharingDenied, .notDetermined])
  75. .isEmpty
  76. }
  77. // NSPredicate, which use during load increment BG from Health store
  78. private var loadBGPredicate: NSPredicate {
  79. // loading only daily bg
  80. let predicateByStartDate = HKQuery.predicateForSamples(
  81. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  82. end: nil,
  83. options: .strictStartDate
  84. )
  85. // loading only not FreeAPS bg
  86. // this predicate dont influence on Deleted Objects, only on added
  87. let predicateByMeta = HKQuery.predicateForObjects(
  88. withMetadataKey: Config.freeAPSMetaKey,
  89. operatorType: .notEqualTo,
  90. value: 1
  91. )
  92. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  93. }
  94. init(resolver: Resolver) {
  95. injectServices(resolver)
  96. guard isAvailableOnCurrentDevice,
  97. Config.healthBGObject != nil else { return }
  98. createBGObserver()
  99. enableBackgroundDelivery()
  100. broadcaster.register(CarbsObserver.self, observer: self)
  101. broadcaster.register(PumpHistoryObserver.self, observer: self)
  102. debug(.service, "HealthKitManager did create")
  103. }
  104. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  105. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  106. }
  107. func checkAvailabilitySaveBG() -> Bool {
  108. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  109. }
  110. func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
  111. guard isAvailableOnCurrentDevice else {
  112. completion?(false, HKError.notAvailableOnCurrentDevice)
  113. return
  114. }
  115. guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
  116. completion?(false, HKError.dataNotAvailable)
  117. return
  118. }
  119. healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
  120. completion?(status, error)
  121. }
  122. }
  123. func saveIfNeeded(bloodGlucose: [BloodGlucose]) {
  124. guard settingsManager.settings.useAppleHealth,
  125. let sampleType = Config.healthBGObject,
  126. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  127. bloodGlucose.isNotEmpty
  128. else { return }
  129. func save(samples: [HKSample]) {
  130. let sampleIDs = samples.compactMap(\.syncIdentifier)
  131. let samplesToSave = bloodGlucose
  132. .filter { !sampleIDs.contains($0.id) }
  133. .map {
  134. HKQuantitySample(
  135. type: sampleType,
  136. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double($0.glucose!)),
  137. start: $0.dateString,
  138. end: $0.dateString,
  139. metadata: [
  140. HKMetadataKeyExternalUUID: $0.id,
  141. HKMetadataKeySyncIdentifier: $0.id,
  142. HKMetadataKeySyncVersion: 1,
  143. Config.freeAPSMetaKey: true
  144. ]
  145. )
  146. }
  147. healthKitStore.save(samplesToSave) { _, _ in }
  148. }
  149. loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id))
  150. .receive(on: processQueue)
  151. .sink(receiveValue: save)
  152. .store(in: &lifetime)
  153. }
  154. func saveIfNeeded(carbs: [CarbsEntry]) {
  155. guard settingsManager.settings.useAppleHealth,
  156. let sampleType = Config.healthCarbObject,
  157. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  158. carbs.isNotEmpty
  159. else { return }
  160. let carbsWithId = carbs.filter { c in
  161. guard c.collectionID != nil else { return false }
  162. return true
  163. }
  164. func save(samples: [HKSample]) {
  165. let sampleIDs = samples.compactMap(\.syncIdentifier)
  166. let sampleDates = samples.map(\.startDate)
  167. let samplesToSave = carbsWithId
  168. .filter { !sampleIDs.contains($0.collectionID!) } // id existing in AH
  169. .filter { !sampleDates.contains($0.createdAt) } // not id but exaclty the same datetime
  170. .map {
  171. HKQuantitySample(
  172. type: sampleType,
  173. quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
  174. start: $0.createdAt,
  175. end: $0.createdAt,
  176. metadata: [
  177. HKMetadataKeyExternalUUID: $0.collectionID ?? "_id",
  178. HKMetadataKeySyncIdentifier: $0.collectionID ?? "_id",
  179. HKMetadataKeySyncVersion: 1,
  180. Config.freeAPSMetaKey: true
  181. ]
  182. )
  183. }
  184. healthKitStore.save(samplesToSave) { _, _ in }
  185. }
  186. loadSamplesFromHealth(sampleType: sampleType)
  187. .receive(on: processQueue)
  188. .sink(receiveValue: save)
  189. .store(in: &lifetime)
  190. }
  191. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
  192. guard settingsManager.settings.useAppleHealth,
  193. let sampleType = Config.healthInsulinObject,
  194. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  195. events.isNotEmpty
  196. else { return }
  197. func save(bolusToModify: [InsulinBolus], bolus: [InsulinBolus], basal: [InsulinBasal]) {
  198. // first step : delete the HK value
  199. // second step : recreate with the new value !
  200. bolusToModify.forEach { syncID in
  201. let predicate = HKQuery.predicateForObjects(
  202. withMetadataKey: HKMetadataKeySyncIdentifier,
  203. operatorType: .equalTo,
  204. value: syncID
  205. )
  206. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  207. guard let error = error else { return }
  208. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  209. }
  210. }
  211. let bolusTotal = bolus + bolusToModify
  212. let bolusSamples = bolusTotal
  213. .map {
  214. HKQuantitySample(
  215. type: sampleType,
  216. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  217. start: $0.date,
  218. end: $0.date,
  219. metadata: [
  220. HKMetadataKeyInsulinDeliveryReason: NSNumber(2),
  221. HKMetadataKeyExternalUUID: $0.id,
  222. HKMetadataKeySyncIdentifier: $0.id,
  223. HKMetadataKeySyncVersion: 1,
  224. Config.freeAPSMetaKey: true
  225. ]
  226. )
  227. }
  228. let basalSamples = basal
  229. .map {
  230. HKQuantitySample(
  231. type: sampleType,
  232. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  233. start: $0.startDelivery,
  234. end: $0.endDelivery,
  235. metadata: [
  236. HKMetadataKeyInsulinDeliveryReason: NSNumber(1),
  237. HKMetadataKeyExternalUUID: $0.id,
  238. HKMetadataKeySyncIdentifier: $0.id,
  239. HKMetadataKeySyncVersion: 1,
  240. Config.freeAPSMetaKey: true
  241. ]
  242. )
  243. }
  244. healthKitStore.save(bolusSamples + basalSamples) { _, _ in }
  245. }
  246. loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
  247. .receive(on: processQueue)
  248. .compactMap { samples -> ([InsulinBolus], [InsulinBolus], [InsulinBasal]) in
  249. let sampleIDs = samples.compactMap(\.syncIdentifier)
  250. let bolusToModify = events
  251. .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
  252. .compactMap { event -> InsulinBolus? in
  253. guard let amount = event.amount else { return nil }
  254. guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
  255. else { return nil }
  256. if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
  257. return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
  258. } else { return nil }
  259. }
  260. let bolus = events
  261. .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
  262. .compactMap { event -> InsulinBolus? in
  263. guard let amount = event.amount else { return nil }
  264. return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
  265. }
  266. let basalEvents = events
  267. .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
  268. .sorted(by: { $0.timestamp < $1.timestamp })
  269. let basal = basalEvents.enumerated()
  270. .compactMap { item -> InsulinBasal? in
  271. let nextElementEventIndex = item.offset + 1
  272. guard basalEvents.count > nextElementEventIndex else { return nil }
  273. var minimalDose = self.settingsManager.preferences.bolusIncrement
  274. if (minimalDose != 0.05) || (minimalDose != 0.025) {
  275. minimalDose = Decimal(0.05)
  276. }
  277. let nextBasalEvent = basalEvents[nextElementEventIndex]
  278. let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
  279. let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
  280. let incrementsRaw = amount / minimalDose
  281. var amountRounded: Decimal
  282. if incrementsRaw >= 1 {
  283. let incrementsRounded = floor(Double(incrementsRaw))
  284. amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
  285. } else {
  286. amountRounded = 0
  287. }
  288. let id = String(item.element.id.dropFirst())
  289. guard amountRounded > 0,
  290. id != ""
  291. else { return nil }
  292. return InsulinBasal(
  293. id: id,
  294. amount: amountRounded,
  295. startDelivery: item.element.timestamp,
  296. endDelivery: nextBasalEvent.timestamp
  297. )
  298. }
  299. return (bolusToModify, bolus, basal)
  300. }
  301. .sink(receiveValue: save)
  302. .store(in: &lifetime)
  303. }
  304. func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent]) {
  305. saveIfNeeded(pumpEvents: events)
  306. }
  307. func createBGObserver() {
  308. guard settingsManager.settings.useAppleHealth else { return }
  309. guard let bgType = Config.healthBGObject else {
  310. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  311. return
  312. }
  313. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  314. guard let self = self else { return }
  315. debug(.service, "Execute HealthKit observer query for loading increment samples")
  316. guard observerError == nil else {
  317. warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!)
  318. return
  319. }
  320. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  321. debug(.service, "Create increment query")
  322. self.healthKitStore.execute(incrementQuery)
  323. }
  324. }
  325. healthKitStore.execute(query)
  326. debug(.service, "Create Observer for Blood Glucose")
  327. }
  328. func enableBackgroundDelivery() {
  329. guard settingsManager.settings.useAppleHealth else {
  330. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  331. return }
  332. guard let bgType = Config.healthBGObject else {
  333. warning(
  334. .service,
  335. "Can not create background delivery, because unable to get the Blood Glucose type"
  336. )
  337. return
  338. }
  339. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  340. guard error == nil else {
  341. warning(.service, "Can not enable background delivery", error: error)
  342. return
  343. }
  344. debug(.service, "Background delivery status is \(status)")
  345. }
  346. }
  347. /// Try to load samples from Health store
  348. private func loadSamplesFromHealth(
  349. sampleType: HKQuantityType,
  350. limit: Int = 100
  351. ) -> Future<[HKSample], Never> {
  352. Future { promise in
  353. let query = HKSampleQuery(
  354. sampleType: sampleType,
  355. predicate: nil,
  356. limit: limit,
  357. sortDescriptors: nil
  358. ) { _, results, _ in
  359. promise(.success((results as? [HKQuantitySample]) ?? []))
  360. }
  361. self.healthKitStore.execute(query)
  362. }
  363. }
  364. /// Try to load samples from Health store with id and do some work
  365. private func loadSamplesFromHealth(
  366. sampleType: HKQuantityType,
  367. withIDs ids: [String],
  368. limit: Int = 100
  369. ) -> Future<[HKSample], Never> {
  370. Future { promise in
  371. let predicate = HKQuery.predicateForObjects(
  372. withMetadataKey: HKMetadataKeySyncIdentifier,
  373. allowedValues: ids
  374. )
  375. let query = HKSampleQuery(
  376. sampleType: sampleType,
  377. predicate: predicate,
  378. limit: limit,
  379. sortDescriptors: nil
  380. ) { _, results, _ in
  381. promise(.success((results as? [HKQuantitySample]) ?? []))
  382. }
  383. self.healthKitStore.execute(query)
  384. }
  385. }
  386. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  387. guard let sampleType = Config.healthBGObject else { return nil }
  388. let query = HKAnchoredObjectQuery(
  389. type: sampleType,
  390. predicate: predicate,
  391. anchor: lastBloodGlucoseQueryAnchor,
  392. limit: HKObjectQueryNoLimit
  393. ) { [weak self] _, addedObjects, _, anchor, _ in
  394. guard let self = self else { return }
  395. self.processQueue.async {
  396. debug(.service, "AnchoredQuery did execute")
  397. self.lastBloodGlucoseQueryAnchor = anchor
  398. // Added objects
  399. if let bgSamples = addedObjects as? [HKQuantitySample],
  400. bgSamples.isNotEmpty
  401. {
  402. self.prepareBGSamplesToPublisherFetch(bgSamples)
  403. }
  404. }
  405. }
  406. return query
  407. }
  408. private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  409. dispatchPrecondition(condition: .onQueue(processQueue))
  410. debug(.service, "Start preparing samples: \(String(describing: samples))")
  411. newGlucose += samples
  412. .compactMap { sample -> HealthKitSample? in
  413. let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  414. guard !fromFAX else { return nil }
  415. return HealthKitSample(
  416. healthKitId: sample.uuid.uuidString,
  417. date: sample.startDate,
  418. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  419. )
  420. }
  421. .map { sample in
  422. BloodGlucose(
  423. _id: sample.healthKitId,
  424. sgv: sample.glucose,
  425. direction: nil,
  426. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  427. dateString: sample.date,
  428. unfiltered: Decimal(sample.glucose),
  429. filtered: nil,
  430. noise: nil,
  431. glucose: sample.glucose,
  432. type: "sgv"
  433. )
  434. }
  435. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  436. newGlucose = newGlucose.removeDublicates()
  437. debug(
  438. .service,
  439. "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
  440. )
  441. }
  442. // MARK: - GlucoseSource
  443. var glucoseManager: FetchGlucoseManager?
  444. var cgmManager: CGMManagerUI?
  445. var cgmType: CGMType = .nightscout
  446. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  447. Future { [weak self] promise in
  448. guard let self = self else {
  449. promise(.success([]))
  450. return
  451. }
  452. self.processQueue.async {
  453. // debug(.service, "Start fetching HealthKitManager")
  454. guard self.settingsManager.settings.useAppleHealth else {
  455. debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
  456. promise(.success([]))
  457. return
  458. }
  459. // Remove old BGs
  460. self.newGlucose = self.newGlucose
  461. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  462. // Get actual BGs (beetwen Date() - 1 day and Date())
  463. let actualGlucose = self.newGlucose
  464. .filter { $0.dateString <= Date() }
  465. // Update newGlucose
  466. self.newGlucose = self.newGlucose
  467. .filter { !actualGlucose.contains($0) }
  468. // debug(.service, "Actual glucose is \(actualGlucose)")
  469. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  470. promise(.success(actualGlucose))
  471. }
  472. }
  473. .eraseToAnyPublisher()
  474. }
  475. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  476. fetch(nil)
  477. }
  478. func deleteGlucose(syncID: String) {
  479. guard settingsManager.settings.useAppleHealth,
  480. let sampleType = Config.healthBGObject,
  481. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  482. else { return }
  483. processQueue.async {
  484. let predicate = HKQuery.predicateForObjects(
  485. withMetadataKey: HKMetadataKeySyncIdentifier,
  486. operatorType: .equalTo,
  487. value: syncID
  488. )
  489. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  490. guard let error = error else { return }
  491. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  492. }
  493. }
  494. }
  495. // - MARK Carbs function
  496. func deleteCarbs(syncID: String, isFPU: Bool?, fpuID: String?) {
  497. guard settingsManager.settings.useAppleHealth,
  498. let sampleType = Config.healthCarbObject,
  499. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  500. else { return }
  501. if let isFPU = isFPU, !isFPU {
  502. processQueue.async {
  503. let predicate = HKQuery.predicateForObjects(
  504. withMetadataKey: HKMetadataKeySyncIdentifier,
  505. operatorType: .equalTo,
  506. value: syncID
  507. )
  508. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  509. guard let error = error else { return }
  510. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  511. }
  512. }
  513. } else {
  514. // need to find all syncID
  515. guard let fpuID = fpuID else { return }
  516. processQueue.async {
  517. let recentCarbs: [CarbsEntry] = self.carbsStorage.recent()
  518. let ids = recentCarbs.filter { $0.fpuID == fpuID }.compactMap(\.id)
  519. let predicate = HKQuery.predicateForObjects(
  520. withMetadataKey: HKMetadataKeySyncIdentifier,
  521. allowedValues: ids
  522. )
  523. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  524. guard let error = error else { return }
  525. warning(.service, "Cannot delete sample with fpuID: \(fpuID)", error: error)
  526. }
  527. }
  528. }
  529. }
  530. func carbsDidUpdate(_ carbs: [CarbsEntry]) {
  531. saveIfNeeded(carbs: carbs)
  532. }
  533. // - MARK Insulin function
  534. func deleteInsulin(syncID: String) {
  535. guard settingsManager.settings.useAppleHealth,
  536. let sampleType = Config.healthInsulinObject,
  537. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  538. else { return }
  539. processQueue.async {
  540. let predicate = HKQuery.predicateForObjects(
  541. withMetadataKey: HKMetadataKeySyncIdentifier,
  542. operatorType: .equalTo,
  543. value: syncID
  544. )
  545. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  546. guard let error = error else { return }
  547. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  548. }
  549. }
  550. }
  551. }
  552. enum HealthKitPermissionRequestStatus {
  553. case needRequest
  554. case didRequest
  555. }
  556. enum HKError: Error {
  557. // HealthKit work only iPhone (not on iPad)
  558. case notAvailableOnCurrentDevice
  559. // Some data can be not available on current iOS-device
  560. case dataNotAvailable
  561. }
  562. private struct InsulinBolus {
  563. var id: String
  564. var amount: Decimal
  565. var date: Date
  566. }
  567. private struct InsulinBasal {
  568. var id: String
  569. var amount: Decimal
  570. var startDelivery: Date
  571. var endDelivery: Date
  572. }