Преглед на файлове

HealthKitManager refactored again

Ivan Valkou преди 4 години
родител
ревизия
1da1c72564
променени са 2 файла, в които са добавени 101 реда и са изтрити 75 реда
  1. 1 1
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  2. 100 74
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

+ 1 - 1
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -62,8 +62,8 @@ extension NightscoutConfig {
                     }
 
                     guard glucose.isNotEmpty else { return }
-                    self.glucoseStorage.storeGlucose(glucose)
                     self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
+                    self.glucoseStorage.storeGlucose(glucose)
                 }
                 .store(in: &lifetime)
         }

+ 100 - 74
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -23,12 +23,6 @@ protocol HealthKitManager: GlucoseSource {
 }
 
 final class BaseHealthKitManager: HealthKitManager, Injectable {
-    @Injected() private var glucoseStorage: GlucoseStorage!
-    @Injected() private var healthKitStore: HKHealthStore!
-    @Injected() private var settingsManager: SettingsManager!
-
-    private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
-
     private enum Config {
         // unwraped HKObjects
         static var permissions: Set<HKSampleType> { Set([healthBGObject].compactMap { $0 }) }
@@ -40,23 +34,19 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         static let freeAPSMetaKey = "fromFreeAPSX"
     }
 
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var healthKitStore: HKHealthStore!
+    @Injected() private var settingsManager: SettingsManager!
+
+    private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
+    private var lifetime = Lifetime()
+
     // BG that will be return Publisher
-    @SyncAccess @Persisted(key: "HealthKitManagerNewGlucose") private var newGlucose: [BloodGlucose] = []
+    @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
 
     // last anchor for HKAnchoredQuery
-    private var lastBloodGlucoseQueryAnchor: HKQueryAnchor! {
-        set {
-            persistedAnchor = (
-                try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
-            ) ?? Data()
-        }
-        get {
-            (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(persistedAnchor) as? HKQueryAnchor) ??
-                HKQueryAnchor(fromValue: 0)
-        }
-    }
-
-    @SyncAccess @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor = Data()
+    @Persisted(key: "BaseHealthKitManager.lastBloodGlucoseQueryAnchor") private var lastBloodGlucoseQueryAnchor =
+        CodableAnchor(anchor: nil)
 
     var isAvailableOnCurrentDevice: Bool {
         HKHealthStore.isHealthDataAvailable()
@@ -127,32 +117,32 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               bloodGlucose.isNotEmpty
         else { return }
 
-        processQueue.async {
-            for bgItem in bloodGlucose {
-                let bgQuantity = HKQuantity(
-                    unit: .milligramsPerDeciliter,
-                    doubleValue: Double(bgItem.glucose!)
-                )
-
-                let bgObjectSample = HKQuantitySample(
-                    type: sampleType,
-                    quantity: bgQuantity,
-                    start: bgItem.dateString,
-                    end: bgItem.dateString,
-                    metadata: [
-                        HKMetadataKeyExternalUUID: bgItem.id,
-                        HKMetadataKeySyncIdentifier: bgItem.id,
-                        HKMetadataKeySyncVersion: 1,
-                        Config.freeAPSMetaKey: true
-                    ]
-                )
-                self.load(sampleFromHealth: sampleType, withID: bgItem.id) { [weak self] samples in
-                    if samples.isEmpty {
-                        self?.healthKitStore.save(bgObjectSample) { _, _ in }
-                    }
+        func save(samples: [HKSample]) {
+            let sampleIDs = samples.compactMap(\.syncIdentifier)
+            let samplesToSave = bloodGlucose
+                .filter { !sampleIDs.contains($0.id) }
+                .map {
+                    HKQuantitySample(
+                        type: sampleType,
+                        quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double($0.glucose!)),
+                        start: $0.dateString,
+                        end: $0.dateString,
+                        metadata: [
+                            HKMetadataKeyExternalUUID: $0.id,
+                            HKMetadataKeySyncIdentifier: $0.id,
+                            HKMetadataKeySyncVersion: 1,
+                            Config.freeAPSMetaKey: true
+                        ]
+                    )
                 }
-            }
+
+            healthKitStore.save(samplesToSave) { _, _ in }
         }
+
+        loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id))
+            .receive(on: processQueue)
+            .sink(receiveValue: save)
+            .store(in: &lifetime)
     }
 
     func createObserver() {
@@ -203,33 +193,26 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     /// Try to load samples from Health store with id and do some work
-    private func load(
-        sampleFromHealth sampleType: HKQuantityType,
-        withID id: String,
-        andDo completion: (([HKSample]) -> Void)?
-    ) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        let predicate = HKQuery.predicateForObjects(
-            withMetadataKey: HKMetadataKeySyncIdentifier,
-            operatorType: .equalTo,
-            value: id
-        )
-
-        let query = HKSampleQuery(
-            sampleType: sampleType,
-            predicate: predicate,
-            limit: 1,
-            sortDescriptors: nil
-        ) { _, results, _ in
+    private func loadSamplesFromHealth(
+        sampleType: HKQuantityType,
+        withIDs ids: [String]
+    ) -> Future<[HKSample], Never> {
+        Future { promise in
+            let predicate = HKQuery.predicateForObjects(
+                withMetadataKey: HKMetadataKeySyncIdentifier,
+                allowedValues: ids
+            )
 
-            guard let samples = results as? [HKQuantitySample] else {
-                completion?([])
-                return
+            let query = HKSampleQuery(
+                sampleType: sampleType,
+                predicate: predicate,
+                limit: 1000,
+                sortDescriptors: nil
+            ) { _, results, _ in
+                promise(.success((results as? [HKQuantitySample]) ?? []))
             }
-
-            completion?(samples)
+            self.healthKitStore.execute(query)
         }
-        healthKitStore.execute(query)
     }
 
     private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
@@ -238,14 +221,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         let query = HKAnchoredObjectQuery(
             type: sampleType,
             predicate: predicate,
-            anchor: lastBloodGlucoseQueryAnchor,
+            anchor: lastBloodGlucoseQueryAnchor.anchor,
             limit: HKObjectQueryNoLimit
         ) { [weak self] _, addedObjects, deletedObjects, anchor, _ in
             guard let self = self else { return }
             self.processQueue.async {
                 debug(.service, "AnchoredQuery did execute")
 
-                self.lastBloodGlucoseQueryAnchor = anchor
+                self.lastBloodGlucoseQueryAnchor = CodableAnchor(anchor: anchor)
 
                 // Added objects
                 if let bgSamples = addedObjects as? [HKQuantitySample],
@@ -304,14 +287,28 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     private func deleteSamplesFromLocalStorage(_ deletedSamples: [HKDeletedObject]) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.service, "Delete HealthKit objects: \(String(describing: deletedSamples))")
+        guard settingsManager.settings.useAppleHealth,
+              let sampleType = Config.healthBGObject,
+              checkAvailabilitySave(objectTypeToHealthStore: sampleType),
+              deletedSamples.isNotEmpty
+        else { return }
 
         let removingBGID = deletedSamples.map {
             $0.metadata?[HKMetadataKeySyncIdentifier] as? String ?? $0.uuid.uuidString
         }
-        glucoseStorage.removeGlucose(ids: removingBGID)
-        newGlucose = newGlucose.filter { !removingBGID.contains($0.id) }
+
+        func delete(samples: [HKSample]) {
+            let sampleIDs = samples.map(\.syncIdentifier)
+            let idsToRemove = removingBGID.filter { !sampleIDs.contains($0) }
+            debug(.service, "Delete HealthKit objects: \(idsToRemove)")
+            glucoseStorage.removeGlucose(ids: idsToRemove)
+            newGlucose = newGlucose.filter { !idsToRemove.contains($0.id) }
+        }
+
+        loadSamplesFromHealth(sampleType: sampleType, withIDs: removingBGID)
+            .receive(on: processQueue)
+            .sink(receiveValue: delete)
+            .store(in: &lifetime)
     }
 
     func fetch() -> AnyPublisher<[BloodGlucose], Never> {
@@ -361,3 +358,32 @@ enum HKError: Error {
     // Some data can be not available on current iOS-device
     case dataNotAvailable
 }
+
+final class CodableAnchor: Codable, Equatable {
+    static func == (lhs: CodableAnchor, rhs: CodableAnchor) -> Bool {
+        lhs.anchor == rhs.anchor
+    }
+
+    enum CodingKeys: String, CodingKey {
+        case data
+    }
+
+    let anchor: HKQueryAnchor
+
+    init(anchor: HKQueryAnchor?) {
+        self.anchor = anchor ?? HKQueryAnchor(fromValue: 0)
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+
+        let data = try NSKeyedArchiver.archivedData(withRootObject: anchor as Any, requiringSecureCoding: false)
+        try container.encode(data, forKey: .data)
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let data = try container.decode(Data.self, forKey: .data)
+        anchor = (try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor) ?? HKQueryAnchor(fromValue: 0)
+    }
+}