Skip to content

Commit 5a772c8

Browse files
author
Darin Krauss
authored
LOOP-1704 Allow glucose data without HealthKit write access (#288)
- https://tidepool.atlassian.net/browse/LOOP-1704 - Make required changes based upon GlucoseStore updates - Refactor watch glucose support - Minor renaming for consistency between stores - Add tests
1 parent 83474a1 commit 5a772c8

22 files changed

+309
-152
lines changed

Common/Models/WatchHistoricalCarbs.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ extension WatchHistoricalCarbs: RawRepresentable {
3939
return encoder
4040
}
4141

42-
private static var decoder: PropertyListDecoder { PropertyListDecoder() }
42+
private static var decoder: PropertyListDecoder = PropertyListDecoder()
4343
}

Common/Models/WatchHistoricalGlucose.swift

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,70 @@ import Foundation
1010
import HealthKit
1111
import LoopKit
1212

13-
1413
struct WatchHistoricalGlucose {
15-
let samples: [NewGlucoseSample]
16-
17-
init(with samples: [StoredGlucoseSample]) {
18-
self.samples = samples.map {
19-
NewGlucoseSample(date: $0.startDate, quantity: $0.quantity, isDisplayOnly: $0.isDisplayOnly, wasUserEntered: $0.wasUserEntered, syncIdentifier: $0.syncIdentifier, syncVersion: 0)
20-
}
21-
}
14+
let samples: [StoredGlucoseSample]
2215
}
2316

24-
2517
extension WatchHistoricalGlucose: RawRepresentable {
2618
typealias RawValue = [String: Any]
2719

20+
init?(rawValue: RawValue) {
21+
guard let rawSamples = rawValue["samples"] as? Data,
22+
let flattened = try? Self.decoder.decode(Flattened.self, from: rawSamples) else {
23+
return nil
24+
}
25+
self.samples = flattened.samples
26+
}
27+
2828
var rawValue: RawValue {
29+
guard let rawSamples = try? Self.encoder.encode(Flattened(samples: samples)) else {
30+
return [:]
31+
}
2932
return [
30-
"d": samples.map { $0.date },
31-
"v": samples.map { Int16($0.quantity.doubleValue(for: .milligramsPerDeciliter)) },
32-
"id": samples.map { $0.syncIdentifier },
33-
"do": samples.map { $0.isDisplayOnly },
34-
"ue": samples.map { $0.wasUserEntered }
33+
"samples": rawSamples
3534
]
3635
}
3736

38-
init?(rawValue: RawValue) {
39-
guard
40-
let dates = rawValue["d"] as? [Date],
41-
let values = rawValue["v"] as? [Int16],
42-
let syncIdentifiers = rawValue["id"] as? [String],
43-
let isDisplayOnly = rawValue["do"] as? [Bool],
44-
let wasUserEntered = rawValue["ue"] as? [Bool],
45-
dates.count == values.count,
46-
dates.count == syncIdentifiers.count,
47-
dates.count == isDisplayOnly.count,
48-
dates.count == wasUserEntered.count
49-
else {
50-
return nil
37+
private struct Flattened: Codable {
38+
let uuids: [UUID?]
39+
let provenanceIdentifiers: [String]
40+
let syncIdentifiers: [String?]
41+
let syncVersions: [Int?]
42+
let startDates: [Date]
43+
let quantities: [Double]
44+
let isDisplayOnlys: [Bool]
45+
let wasUserEntereds: [Bool]
46+
47+
init(samples: [StoredGlucoseSample]) {
48+
self.uuids = samples.map { $0.uuid }
49+
self.provenanceIdentifiers = samples.map { $0.provenanceIdentifier }
50+
self.syncIdentifiers = samples.map { $0.syncIdentifier }
51+
self.syncVersions = samples.map { $0.syncVersion }
52+
self.startDates = samples.map { $0.startDate }
53+
self.quantities = samples.map { $0.quantity.doubleValue(for: .milligramsPerDeciliter) }
54+
self.isDisplayOnlys = samples.map { $0.isDisplayOnly }
55+
self.wasUserEntereds = samples.map { $0.wasUserEntered }
5156
}
5257

53-
self.samples = (0..<dates.count).map {
54-
NewGlucoseSample(date: dates[$0], quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(values[$0])), isDisplayOnly: isDisplayOnly[$0], wasUserEntered: wasUserEntered[$0], syncIdentifier: syncIdentifiers[$0], syncVersion: 0)
58+
var samples: [StoredGlucoseSample] {
59+
return (0..<uuids.count).map {
60+
StoredGlucoseSample(uuid: uuids[$0],
61+
provenanceIdentifier: provenanceIdentifiers[$0],
62+
syncIdentifier: syncIdentifiers[$0],
63+
syncVersion: syncVersions[$0],
64+
startDate: startDates[$0],
65+
quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: quantities[$0]),
66+
isDisplayOnly: isDisplayOnlys[$0],
67+
wasUserEntered: wasUserEntereds[$0])
68+
}
5569
}
5670
}
71+
72+
private static var encoder: PropertyListEncoder {
73+
let encoder = PropertyListEncoder()
74+
encoder.outputFormat = .binary
75+
return encoder
76+
}
77+
78+
private static var decoder: PropertyListDecoder = PropertyListDecoder()
5779
}

Learn/Lessons/ModalDayLesson.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ fileprivate class ModalDayCalculator {
180180
dataManager.glucoseStore.getGlucoseSamples(start: day.start, end: day.end, completion: { (result) in
181181
switch result {
182182
case .failure(let error):
183-
os_log(.error, log: self.log, "Failed to fetch samples: %{public}@", String(describing: error))
183+
os_log(.error, log: self.log, "Failure getting glucose samples: %{public}@", String(describing: error))
184184
completion(error)
185185
case .success(let samples):
186186
os_log(.error, log: self.log, "Found %d samples", samples.count)

Learn/Lessons/TimeInRangeLesson.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ private class TimeInRangeCalculator {
174174
dataManager.glucoseStore.getGlucoseSamples(start: day.start, end: day.end) { (result) in
175175
switch result {
176176
case .failure(let error):
177-
os_log(.error, log: self.log, "Failed to fetch samples: %{public}@", String(describing: error))
177+
os_log(.error, log: self.log, "Failure getting glucose samples: %{public}@", String(describing: error))
178178
completion(error)
179179
case .success(let samples):
180180
if let timeInRange = samples.proportion(where: { self.range.contains($0.quantity) }) {

Learn/Managers/DataManager.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ final class DataManager {
5555
glucoseStore = GlucoseStore(
5656
healthStore: healthStore,
5757
cacheStore: cacheStore,
58-
observationEnabled: false
58+
observationEnabled: false,
59+
provenanceIdentifier: HKSource.default().bundleIdentifier
5960
)
6061
}
6162
}

Loop Status Extension/StatusViewController.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ class StatusViewController: UIViewController, NCWidgetProviding {
7878
healthStore: healthStore,
7979
observeHealthKitSamplesFromOtherApps: FeatureFlags.observeHealthKitSamplesFromOtherApps,
8080
cacheStore: cacheStore,
81-
observationEnabled: false
81+
observationEnabled: false,
82+
provenanceIdentifier: HKSource.default().bundleIdentifier
8283
)
8384

8485
lazy var doseStore = DoseStore(
@@ -220,8 +221,13 @@ class StatusViewController: UIViewController, NCWidgetProviding {
220221
charts.maxEndDate = charts.startDate.addingTimeInterval(TimeInterval(hours: 3))
221222

222223
group.enter()
223-
glucoseStore.getCachedGlucoseSamples(start: charts.startDate) { (result) in
224-
glucose = result
224+
glucoseStore.getGlucoseSamples(start: charts.startDate) { (result) in
225+
switch result {
226+
case .failure:
227+
glucose = []
228+
case .success(let samples):
229+
glucose = samples
230+
}
225231
group.leave()
226232
}
227233

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@
397397
A9A63F8D246B261100588D5B /* DosingDecisionStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A63F8C246B261100588D5B /* DosingDecisionStoreTests.swift */; };
398398
A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; };
399399
A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */; };
400+
A9C1719725366F780053BCBD /* WatchHistoricalGlucoseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */; };
400401
A9C62D8223316FF600535612 /* UserDefaults+Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C62D8123316FF500535612 /* UserDefaults+Services.swift */; };
401402
A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C62D832331700D00535612 /* DiagnosticLog+Subsystem.swift */; };
402403
A9C62D882331703100535612 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C62D852331703000535612 /* Service.swift */; };
@@ -1262,6 +1263,7 @@
12621263
A9A056B424B94123007CF06D /* CriticalEventLogExportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportViewModel.swift; sourceTree = "<group>"; };
12631264
A9A63F8C246B261100588D5B /* DosingDecisionStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DosingDecisionStoreTests.swift; sourceTree = "<group>"; };
12641265
A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserNotifications+Loop.swift"; sourceTree = "<group>"; };
1266+
A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHistoricalGlucoseTest.swift; sourceTree = "<group>"; };
12651267
A9C62D8123316FF500535612 /* UserDefaults+Services.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Services.swift"; sourceTree = "<group>"; };
12661268
A9C62D832331700D00535612 /* DiagnosticLog+Subsystem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DiagnosticLog+Subsystem.swift"; sourceTree = "<group>"; };
12671269
A9C62D852331703000535612 /* Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; };
@@ -2337,6 +2339,7 @@
23372339
A9E6DFEE246A0474005B1A1C /* LoopErrorTests.swift */,
23382340
A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */,
23392341
A9DFAFB424F048A000950D1E /* WatchHistoricalCarbsTests.swift */,
2342+
A9C1719625366F780053BCBD /* WatchHistoricalGlucoseTest.swift */,
23402343
);
23412344
path = Models;
23422345
sourceTree = "<group>";
@@ -3623,6 +3626,7 @@
36233626
1DFE9E172447B6270082C280 /* UserNotificationAlertPresenterTests.swift in Sources */,
36243627
B4BC56382518DEA900373647 /* CGMStatusHUDViewModelTests.swift in Sources */,
36253628
A9E6DFE8246A043D005B1A1C /* DoseStoreTests.swift in Sources */,
3629+
A9C1719725366F780053BCBD /* WatchHistoricalGlucoseTest.swift in Sources */,
36263630
E93E86B224DDE21D00FF40C8 /* MockCarbStore.swift in Sources */,
36273631
);
36283632
runOnlyForDeploymentPostprocessing = 0;

Loop/Extensions/CarbStore+SimulatedCoreData.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extension CarbStore {
5656
}
5757

5858
func purgeHistoricalCarbObjects(completion: @escaping (Error?) -> Void) {
59-
purgeCarbObjectsUnconditionally(before: historicalEndDate, completion: completion)
59+
purgeCachedCarbObjectsUnconditionally(before: historicalEndDate, completion: completion)
6060
}
6161
}
6262

Loop/Extensions/DeviceDataManager+BolusEntryViewModelDelegate.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ extension DeviceDataManager: BolusEntryViewModelDelegate {
1616
loopManager.getLoopState { block($1) }
1717
}
1818

19-
func addGlucose(_ samples: [NewGlucoseSample], completion: ((Result<[GlucoseValue]>) -> Void)?) {
20-
loopManager.addGlucose(samples, completion: completion)
19+
func addGlucoseSamples(_ samples: [NewGlucoseSample], completion: ((Swift.Result<[StoredGlucoseSample], Error>) -> Void)?) {
20+
loopManager.addGlucoseSamples(samples, completion: completion)
2121
}
2222

2323
func addCarbEntry(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry?, completion: @escaping (Result<StoredCarbEntry>) -> Void) {
@@ -31,8 +31,8 @@ extension DeviceDataManager: BolusEntryViewModelDelegate {
3131
/// func enactBolus(units: Double, at startDate: Date, completion: @escaping (_ error: Error?) -> Void)
3232
/// is already implemented in DeviceDataManager
3333

34-
func getCachedGlucoseSamples(start: Date, end: Date?, completion: @escaping ([StoredGlucoseSample]) -> Void) {
35-
glucoseStore.getCachedGlucoseSamples(start: start, end: end, completion: completion)
34+
func getGlucoseSamples(start: Date?, end: Date?, completion: @escaping (Swift.Result<[StoredGlucoseSample], Error>) -> Void) {
35+
glucoseStore.getGlucoseSamples(start: start, end: end, completion: completion)
3636
}
3737

3838
func insulinOnBoard(at date: Date, completion: @escaping (DoseStoreResult<InsulinValue>) -> Void) {

Loop/Extensions/GlucoseStore+SimulatedCoreData.swift

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ extension GlucoseStore {
2525
var startDate = Calendar.current.startOfDay(for: earliestCacheDate)
2626
let endDate = Calendar.current.startOfDay(for: historicalEndDate)
2727
var value = 0.0
28-
var simulated = [StoredGlucoseSample]()
28+
var simulated = [NewGlucoseSample]()
2929

3030
while startDate < endDate {
31-
simulated.append(StoredGlucoseSample.simulated(startDate: startDate, value: simulatedValueBase + simulatedValueAmplitude * sin(value)))
31+
simulated.append(NewGlucoseSample.simulated(date: startDate, value: simulatedValueBase + simulatedValueAmplitude * sin(value)))
3232

3333
if simulated.count >= simulatedLimit {
3434
if let error = addSimulatedHistoricalGlucoseObjects(samples: simulated) {
@@ -45,10 +45,10 @@ extension GlucoseStore {
4545
completion(addSimulatedHistoricalGlucoseObjects(samples: simulated))
4646
}
4747

48-
private func addSimulatedHistoricalGlucoseObjects(samples: [StoredGlucoseSample]) -> Error? {
48+
private func addSimulatedHistoricalGlucoseObjects(samples: [NewGlucoseSample]) -> Error? {
4949
var addError: Error?
5050
let semaphore = DispatchSemaphore(value: 0)
51-
addGlucoseSamples(samples: samples) { error in
51+
addNewGlucoseSamples(samples: samples) { error in
5252
addError = error
5353
semaphore.signal()
5454
}
@@ -61,15 +61,12 @@ extension GlucoseStore {
6161
}
6262
}
6363

64-
fileprivate extension StoredGlucoseSample {
65-
static func simulated(startDate: Date, value: Double, unit: HKUnit = HKUnit.milligramsPerDeciliter) -> StoredGlucoseSample {
66-
return StoredGlucoseSample(sampleUUID: UUID(),
67-
syncIdentifier: UUID().uuidString,
68-
syncVersion: 1,
69-
startDate: startDate,
70-
quantity: HKQuantity(unit: unit, doubleValue: value),
71-
isDisplayOnly: false,
72-
wasUserEntered: false,
73-
provenanceIdentifier: Bundle.main.bundleIdentifier!)
64+
fileprivate extension NewGlucoseSample {
65+
static func simulated(date: Date, value: Double, unit: HKUnit = HKUnit.milligramsPerDeciliter) -> NewGlucoseSample {
66+
return NewGlucoseSample(date: date,
67+
quantity: HKQuantity(unit: unit, doubleValue: value),
68+
isDisplayOnly: false,
69+
wasUserEntered: false,
70+
syncIdentifier: UUID().uuidString)
7471
}
7572
}

0 commit comments

Comments
 (0)