HealthKitManager.swift 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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 Open-iAPS
  22. func createBGObserver()
  23. /// Enable background delivering objects from Apple Health to Open-iAPS
  24. func enableBackgroundDelivery()
  25. /// Delete glucose with syncID
  26. func deleteGlucose(syncID: String)
  27. /// delete carbs with syncID
  28. func deleteCarbs(syncID: String, 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 = "From Open-iAPS"
  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. broadcaster.register(CarbsObserver.self, observer: self)
  99. broadcaster.register(PumpHistoryObserver.self, observer: self)
  100. debug(.service, "HealthKitManager did create")
  101. }
  102. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  103. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  104. }
  105. func checkAvailabilitySaveBG() -> Bool {
  106. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  107. }
  108. func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
  109. guard isAvailableOnCurrentDevice else {
  110. completion?(false, HKError.notAvailableOnCurrentDevice)
  111. return
  112. }
  113. guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
  114. completion?(false, HKError.dataNotAvailable)
  115. return
  116. }
  117. healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
  118. completion?(status, error)
  119. }
  120. }
  121. func saveIfNeeded(bloodGlucose: [BloodGlucose]) {
  122. guard settingsManager.settings.useAppleHealth,
  123. let sampleType = Config.healthBGObject,
  124. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  125. bloodGlucose.isNotEmpty
  126. else { return }
  127. func save(samples: [HKSample]) {
  128. let sampleIDs = samples.compactMap(\.syncIdentifier)
  129. let samplesToSave = bloodGlucose
  130. .filter { !sampleIDs.contains($0.id) }
  131. .map {
  132. HKQuantitySample(
  133. type: sampleType,
  134. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double($0.glucose!)),
  135. start: $0.dateString,
  136. end: $0.dateString,
  137. metadata: [
  138. HKMetadataKeyExternalUUID: $0.id,
  139. HKMetadataKeySyncIdentifier: $0.id,
  140. HKMetadataKeySyncVersion: 1,
  141. Config.freeAPSMetaKey: true
  142. ]
  143. )
  144. }
  145. healthKitStore.save(samplesToSave) { (success: Bool, error: Error?) -> Void in
  146. if success {
  147. for sample in samplesToSave {
  148. debug(
  149. .service,
  150. "Stored blood glucose \(sample.quantity) in HealthKit Store! Metadata: \(String(describing: sample.metadata?.values))"
  151. )
  152. }
  153. } else {
  154. debug(.service, "Failed to store blood glucose in HealthKit Store!")
  155. debug(.service, error?.localizedDescription ?? "Unknown error")
  156. }
  157. }
  158. }
  159. loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id), completion: { samples in
  160. save(samples: samples)
  161. })
  162. }
  163. func saveIfNeeded(carbs: [CarbsEntry]) {
  164. guard settingsManager.settings.useAppleHealth,
  165. let sampleType = Config.healthCarbObject,
  166. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  167. carbs.isNotEmpty
  168. else { return }
  169. let carbsWithId = carbs.filter { c in
  170. guard c.id != nil else { return false }
  171. return true
  172. }
  173. func save(samples: [HKSample]) {
  174. let sampleIDs = samples.compactMap(\.syncIdentifier)
  175. let sampleDates = samples.map(\.startDate)
  176. let samplesToSave = carbsWithId
  177. .filter { !sampleIDs.contains($0.id ?? "") } // id existing in AH
  178. .filter { !sampleDates.contains($0.actualDate ?? $0.createdAt) } // not id but exactly the same datetime
  179. .map {
  180. HKQuantitySample(
  181. type: sampleType,
  182. quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
  183. start: $0.actualDate ?? $0.createdAt,
  184. end: $0.actualDate ?? $0.createdAt,
  185. metadata: [
  186. HKMetadataKeySyncIdentifier: $0.id ?? "_id",
  187. HKMetadataKeySyncVersion: 1,
  188. Config.freeAPSMetaKey: true
  189. ]
  190. )
  191. }
  192. healthKitStore.save(samplesToSave) { (success: Bool, error: Error?) -> Void in
  193. if success {
  194. for sample in samplesToSave {
  195. debug(
  196. .service,
  197. "Stored carb entry \(sample.quantity) in HealthKit Store! Metadata: \(String(describing: sample.metadata?.values))"
  198. )
  199. }
  200. } else {
  201. debug(.service, "Failed to store carb entry in HealthKit Store!")
  202. debug(.service, error?.localizedDescription ?? "Unknown error")
  203. }
  204. }
  205. }
  206. loadSamplesFromHealth(sampleType: sampleType, completion: { samples in
  207. save(samples: samples)
  208. })
  209. }
  210. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
  211. guard settingsManager.settings.useAppleHealth,
  212. let sampleType = Config.healthInsulinObject,
  213. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  214. events.isNotEmpty
  215. else { return }
  216. func save(bolusToModify: [InsulinBolus], bolus: [InsulinBolus], basal: [InsulinBasal]) {
  217. // first step : delete the HK value
  218. // second step : recreate with the new value !
  219. bolusToModify.forEach { syncID in
  220. let predicate = HKQuery.predicateForObjects(
  221. withMetadataKey: HKMetadataKeySyncIdentifier,
  222. operatorType: .equalTo,
  223. value: syncID.id
  224. )
  225. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  226. guard let error = error else { return }
  227. warning(.service, "Cannot delete sample with syncID: \(syncID.id)", error: error)
  228. }
  229. }
  230. let bolusTotal = bolus + bolusToModify
  231. let bolusSamples = bolusTotal
  232. .map {
  233. HKQuantitySample(
  234. type: sampleType,
  235. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  236. start: $0.date,
  237. end: $0.date,
  238. metadata: [
  239. HKMetadataKeyInsulinDeliveryReason: NSNumber(2),
  240. HKMetadataKeyExternalUUID: $0.id,
  241. HKMetadataKeySyncIdentifier: $0.id,
  242. HKMetadataKeySyncVersion: 1,
  243. Config.freeAPSMetaKey: true
  244. ]
  245. )
  246. }
  247. let basalSamples = basal
  248. .map {
  249. HKQuantitySample(
  250. type: sampleType,
  251. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  252. start: $0.startDelivery,
  253. end: $0.endDelivery,
  254. metadata: [
  255. HKMetadataKeyInsulinDeliveryReason: NSNumber(1),
  256. HKMetadataKeyExternalUUID: $0.id,
  257. HKMetadataKeySyncIdentifier: $0.id,
  258. HKMetadataKeySyncVersion: 1,
  259. Config.freeAPSMetaKey: true
  260. ]
  261. )
  262. }
  263. healthKitStore.save(bolusSamples + basalSamples) { (success: Bool, error: Error?) -> Void in
  264. if success {
  265. for sample in bolusSamples + basalSamples {
  266. debug(
  267. .service,
  268. "Stored insulin entry in HealthKit Store! Metadata: \(String(describing: sample.metadata?.values))"
  269. )
  270. }
  271. } else {
  272. debug(.service, "Failed to store insulin entry in HealthKit Store!")
  273. debug(.service, error?.localizedDescription ?? "Unknown error")
  274. }
  275. }
  276. }
  277. loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id), completion: { samples in
  278. let sampleIDs = samples.compactMap(\.syncIdentifier)
  279. let bolusToModify = events
  280. .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
  281. .compactMap { event -> InsulinBolus? in
  282. guard let amount = event.amount else { return nil }
  283. guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
  284. else { return nil }
  285. if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
  286. return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
  287. } else { return nil }
  288. }
  289. let bolus = events
  290. .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
  291. .compactMap { event -> InsulinBolus? in
  292. guard let amount = event.amount else { return nil }
  293. return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
  294. }
  295. let basalEvents = events
  296. .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
  297. .sorted(by: { $0.timestamp < $1.timestamp })
  298. let basal = basalEvents.enumerated()
  299. .compactMap { item -> InsulinBasal? in
  300. let nextElementEventIndex = item.offset + 1
  301. guard basalEvents.count > nextElementEventIndex else { return nil }
  302. var minimalDose = self.settingsManager.preferences.bolusIncrement
  303. if (minimalDose != 0.05) || (minimalDose != 0.025) {
  304. minimalDose = Decimal(0.05)
  305. }
  306. let nextBasalEvent = basalEvents[nextElementEventIndex]
  307. let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
  308. let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
  309. let incrementsRaw = amount / minimalDose
  310. var amountRounded: Decimal
  311. if incrementsRaw >= 1 {
  312. let incrementsRounded = floor(Double(incrementsRaw))
  313. amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
  314. } else {
  315. amountRounded = 0
  316. }
  317. let id = String(item.element.id.dropFirst())
  318. guard amountRounded > 0,
  319. id != ""
  320. else { return nil }
  321. return InsulinBasal(
  322. id: id,
  323. amount: amountRounded,
  324. startDelivery: item.element.timestamp,
  325. endDelivery: nextBasalEvent.timestamp
  326. )
  327. }
  328. save(bolusToModify: bolusToModify, bolus: bolus, basal: basal)
  329. })
  330. }
  331. func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent]) {
  332. saveIfNeeded(pumpEvents: events)
  333. }
  334. func createBGObserver() {
  335. guard settingsManager.settings.useAppleHealth else { return }
  336. guard let bgType = Config.healthBGObject else {
  337. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  338. return
  339. }
  340. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  341. guard let self = self else { return }
  342. debug(.service, "Execute HealthKit observer query for loading increment samples")
  343. guard observerError == nil else {
  344. warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!)
  345. return
  346. }
  347. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  348. debug(.service, "Create increment query")
  349. self.healthKitStore.execute(incrementQuery)
  350. }
  351. }
  352. healthKitStore.execute(query)
  353. debug(.service, "Create Observer for Blood Glucose")
  354. }
  355. func enableBackgroundDelivery() {
  356. guard settingsManager.settings.useAppleHealth else {
  357. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  358. return }
  359. guard let bgType = Config.healthBGObject else {
  360. warning(
  361. .service,
  362. "Can not create background delivery, because unable to get the Blood Glucose type"
  363. )
  364. return
  365. }
  366. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  367. guard error == nil else {
  368. warning(.service, "Can not enable background delivery", error: error)
  369. return
  370. }
  371. debug(.service, "Background delivery status is \(status)")
  372. }
  373. }
  374. /// Try to load samples from Health store
  375. private func loadSamplesFromHealth(
  376. sampleType: HKQuantityType,
  377. limit: Int = 100,
  378. completion: @escaping (_ samples: [HKSample]) -> Void
  379. ) {
  380. let query = HKSampleQuery(
  381. sampleType: sampleType,
  382. predicate: nil,
  383. limit: limit,
  384. sortDescriptors: nil
  385. ) { _, results, _ in
  386. completion(results as? [HKQuantitySample] ?? [])
  387. }
  388. healthKitStore.execute(query)
  389. }
  390. /// Try to load samples from Health store with id and do some work
  391. private func loadSamplesFromHealth(
  392. sampleType: HKQuantityType,
  393. withIDs ids: [String],
  394. limit: Int = 100,
  395. completion: @escaping (_ samples: [HKSample]) -> Void
  396. ) {
  397. let predicate = HKQuery.predicateForObjects(
  398. withMetadataKey: HKMetadataKeySyncIdentifier,
  399. allowedValues: ids
  400. )
  401. let query = HKSampleQuery(
  402. sampleType: sampleType,
  403. predicate: predicate,
  404. limit: limit,
  405. sortDescriptors: nil
  406. ) { _, results, _ in
  407. completion(results as? [HKQuantitySample] ?? [])
  408. }
  409. healthKitStore.execute(query)
  410. }
  411. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  412. guard let sampleType = Config.healthBGObject else { return nil }
  413. let query = HKAnchoredObjectQuery(
  414. type: sampleType,
  415. predicate: predicate,
  416. anchor: lastBloodGlucoseQueryAnchor,
  417. limit: HKObjectQueryNoLimit
  418. ) { [weak self] _, addedObjects, _, anchor, _ in
  419. guard let self = self else { return }
  420. self.processQueue.async {
  421. debug(.service, "AnchoredQuery did execute")
  422. self.lastBloodGlucoseQueryAnchor = anchor
  423. // Added objects
  424. if let bgSamples = addedObjects as? [HKQuantitySample],
  425. bgSamples.isNotEmpty
  426. {
  427. self.prepareBGSamplesToPublisherFetch(bgSamples)
  428. }
  429. }
  430. }
  431. return query
  432. }
  433. private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  434. dispatchPrecondition(condition: .onQueue(processQueue))
  435. debug(.service, "Start preparing samples: \(String(describing: samples))")
  436. newGlucose += samples
  437. .compactMap { sample -> HealthKitSample? in
  438. let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  439. guard !fromFAX else { return nil }
  440. return HealthKitSample(
  441. healthKitId: sample.uuid.uuidString,
  442. date: sample.startDate,
  443. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  444. )
  445. }
  446. .map { sample in
  447. BloodGlucose(
  448. _id: sample.healthKitId,
  449. sgv: sample.glucose,
  450. direction: nil,
  451. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  452. dateString: sample.date,
  453. unfiltered: Decimal(sample.glucose),
  454. filtered: nil,
  455. noise: nil,
  456. glucose: sample.glucose,
  457. type: "sgv"
  458. )
  459. }
  460. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  461. newGlucose = newGlucose.removeDublicates()
  462. debug(
  463. .service,
  464. "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
  465. )
  466. }
  467. // MARK: - GlucoseSource
  468. var glucoseManager: FetchGlucoseManager?
  469. var cgmManager: CGMManagerUI?
  470. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  471. Future { [weak self] promise in
  472. guard let self = self else {
  473. promise(.success([]))
  474. return
  475. }
  476. self.processQueue.async {
  477. // debug(.service, "Start fetching HealthKitManager")
  478. guard self.settingsManager.settings.useAppleHealth else {
  479. debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
  480. promise(.success([]))
  481. return
  482. }
  483. // Remove old BGs
  484. self.newGlucose = self.newGlucose
  485. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  486. // Get actual BGs (beetwen Date() - 1 day and Date())
  487. let actualGlucose = self.newGlucose
  488. .filter { $0.dateString <= Date() }
  489. // Update newGlucose
  490. self.newGlucose = self.newGlucose
  491. .filter { !actualGlucose.contains($0) }
  492. // debug(.service, "Actual glucose is \(actualGlucose)")
  493. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  494. promise(.success(actualGlucose))
  495. }
  496. }
  497. .eraseToAnyPublisher()
  498. }
  499. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  500. fetch(nil)
  501. }
  502. func deleteGlucose(syncID: String) {
  503. guard settingsManager.settings.useAppleHealth,
  504. let sampleType = Config.healthBGObject,
  505. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  506. else { return }
  507. processQueue.async {
  508. let predicate = HKQuery.predicateForObjects(
  509. withMetadataKey: HKMetadataKeySyncIdentifier,
  510. operatorType: .equalTo,
  511. value: syncID
  512. )
  513. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  514. guard let error = error else { return }
  515. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  516. }
  517. }
  518. }
  519. // - MARK Carbs function
  520. func deleteCarbs(syncID: String, fpuID: String) {
  521. guard settingsManager.settings.useAppleHealth,
  522. let sampleType = Config.healthCarbObject,
  523. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  524. else { return }
  525. print("meals 4: ID: " + syncID + " FPU ID: " + fpuID)
  526. if syncID != "" {
  527. let predicate = HKQuery.predicateForObjects(
  528. withMetadataKey: HKMetadataKeySyncIdentifier,
  529. operatorType: .equalTo,
  530. value: syncID
  531. )
  532. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  533. guard let error = error else { return }
  534. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  535. }
  536. }
  537. if fpuID != "" {
  538. // processQueue.async {
  539. let recentCarbs: [CarbsEntry] = carbsStorage.recent()
  540. let ids = recentCarbs.filter { $0.fpuID == fpuID }.compactMap(\.id)
  541. let predicate = HKQuery.predicateForObjects(
  542. withMetadataKey: HKMetadataKeySyncIdentifier,
  543. allowedValues: ids
  544. )
  545. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  546. guard let error = error else { return }
  547. warning(.service, "Cannot delete sample with fpuID: \(fpuID)", error: error)
  548. }
  549. // }
  550. }
  551. }
  552. func carbsDidUpdate(_ carbs: [CarbsEntry]) {
  553. saveIfNeeded(carbs: carbs)
  554. }
  555. // - MARK Insulin function
  556. func deleteInsulin(syncID: String) {
  557. guard settingsManager.settings.useAppleHealth,
  558. let sampleType = Config.healthInsulinObject,
  559. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  560. else { return }
  561. processQueue.async {
  562. let predicate = HKQuery.predicateForObjects(
  563. withMetadataKey: HKMetadataKeySyncIdentifier,
  564. operatorType: .equalTo,
  565. value: syncID
  566. )
  567. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  568. guard let error = error else { return }
  569. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  570. }
  571. }
  572. }
  573. }
  574. enum HealthKitPermissionRequestStatus {
  575. case needRequest
  576. case didRequest
  577. }
  578. enum HKError: Error {
  579. // HealthKit work only iPhone (not on iPad)
  580. case notAvailableOnCurrentDevice
  581. // Some data can be not available on current iOS-device
  582. case dataNotAvailable
  583. }
  584. private struct InsulinBolus {
  585. var id: String
  586. var amount: Decimal
  587. var date: Date
  588. }
  589. private struct InsulinBasal {
  590. var id: String
  591. var amount: Decimal
  592. var startDelivery: Date
  593. var endDelivery: Date
  594. }