HealthStoreUnitCache.swift 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. //
  2. // HealthStoreUnitCache.swift
  3. // LoopKit
  4. //
  5. // Copyright © 2018 LoopKit Authors. All rights reserved.
  6. //
  7. import HealthKit
  8. import os.log
  9. public extension Notification.Name {
  10. // used to avoid potential timing issues since a unit change triggers a cache refresh and all stores pull the current unit from the cache
  11. static let HealthStorePreferredGlucoseUnitDidChange = Notification.Name(rawValue: "com.loopKit.notification.HealthStorePreferredGlucoseUnitDidChange")
  12. }
  13. public class HealthStoreUnitCache {
  14. private static var cacheCache = NSMapTable<HKHealthStore, HealthStoreUnitCache>.weakToStrongObjects()
  15. public let healthStore: HKHealthStore
  16. private static let fixedUnits: [HKQuantityTypeIdentifier: HKUnit] = [
  17. .dietaryCarbohydrates: .gram(),
  18. .insulinDelivery: .internationalUnit()
  19. ]
  20. private var unitCache = Locked([HKQuantityTypeIdentifier: HKUnit]())
  21. private var userPreferencesChangeObserver: Any?
  22. private init(healthStore: HKHealthStore) {
  23. self.healthStore = healthStore
  24. userPreferencesChangeObserver = NotificationCenter.default.addObserver(forName: .HKUserPreferencesDidChange, object: healthStore, queue: nil, using: { [weak self] _ in
  25. DispatchQueue.global().async {
  26. self?.updateCachedUnits()
  27. }
  28. })
  29. }
  30. public class func unitCache(for healthStore: HKHealthStore) -> HealthStoreUnitCache {
  31. if let cache = cacheCache.object(forKey: healthStore) {
  32. return cache
  33. }
  34. let cache = HealthStoreUnitCache(healthStore: healthStore)
  35. cacheCache.setObject(cache, forKey: healthStore)
  36. return cache
  37. }
  38. public func preferredUnit(for quantityTypeIdentifier: HKQuantityTypeIdentifier) -> HKUnit? {
  39. if let unit = HealthStoreUnitCache.fixedUnits[quantityTypeIdentifier] {
  40. return unit
  41. }
  42. if let unit = unitCache.value[quantityTypeIdentifier] {
  43. return unit
  44. }
  45. return getHealthStoreUnitAndUpdateCache(for: quantityTypeIdentifier)
  46. }
  47. @discardableResult private func getHealthStoreUnitAndUpdateCache(for quantityTypeIdentifier: HKQuantityTypeIdentifier) -> HKUnit? {
  48. guard let quantityType = HKQuantityType.quantityType(forIdentifier: quantityTypeIdentifier) else {
  49. return nil
  50. }
  51. var unit: HKUnit?
  52. let semaphore = DispatchSemaphore(value: 0)
  53. healthStore.preferredUnits(for: [quantityType]) { (results, error) in
  54. if let error = error {
  55. // This is a common/expected case when protected data is unavailable
  56. OSLog(category: "HealthStoreUnitCache").info("Error fetching unit for %{public}@: %{public}@", quantityTypeIdentifier.rawValue, String(describing: error))
  57. }
  58. unit = results[quantityType]
  59. self.updateCache(for: quantityTypeIdentifier, with: unit)
  60. semaphore.signal()
  61. }
  62. _ = semaphore.wait(timeout: .now() + .seconds(3))
  63. return unit
  64. }
  65. private func updateCachedUnits() {
  66. let quantityTypeIdentifiers = unitCache.value.keys
  67. for quantityTypeIdentifier in quantityTypeIdentifiers {
  68. self.getHealthStoreUnitAndUpdateCache(for: quantityTypeIdentifier)
  69. }
  70. }
  71. private func updateCache(for quantityTypeIdentifier: HKQuantityTypeIdentifier, with unit: HKUnit?) {
  72. _ = self.unitCache.mutate({ (cache) in
  73. guard unit != cache[quantityTypeIdentifier] else {
  74. return
  75. }
  76. cache[quantityTypeIdentifier] = unit
  77. switch quantityTypeIdentifier {
  78. case .bloodGlucose:
  79. // currently only changes to glucose unit is reported
  80. DispatchQueue.main.async {
  81. NotificationCenter.default.post(name: .HealthStorePreferredGlucoseUnitDidChange, object: self.healthStore)
  82. }
  83. default:
  84. break
  85. }
  86. })
  87. }
  88. deinit {
  89. if let observer = userPreferencesChangeObserver {
  90. NotificationCenter.default.removeObserver(observer)
  91. }
  92. }
  93. }