Skip to content

Commit b3593b2

Browse files
authored
[LOOP 914] Support pre-meal + presets (#56)
* Pre-meal + overrides (w/o watch testing) * Pre-meal + presets support on Watch * Updates from review; ensure future override target ranges are considered * Add LoopSettingsTests
1 parent 521b829 commit b3593b2

File tree

13 files changed

+422
-82
lines changed

13 files changed

+422
-82
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@
327327
892FB4CF220402C0005293EC /* OverrideSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FB4CE220402C0005293EC /* OverrideSelectionController.swift */; };
328328
895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895FE0942201234000FCF18A /* OverrideSelectionViewController.swift */; };
329329
8968B1122408B3520074BB48 /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8968B1112408B3520074BB48 /* UIFont.swift */; };
330+
8968B114240C55F10074BB48 /* LoopSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8968B113240C55F10074BB48 /* LoopSettingsTests.swift */; };
330331
898ECA60218ABD17001E9D35 /* GlucoseChartScaler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898ECA5E218ABD17001E9D35 /* GlucoseChartScaler.swift */; };
331332
898ECA61218ABD17001E9D35 /* GlucoseChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898ECA5F218ABD17001E9D35 /* GlucoseChartData.swift */; };
332333
898ECA63218ABD21001E9D35 /* ComplicationChartManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898ECA62218ABD21001E9D35 /* ComplicationChartManager.swift */; };
@@ -973,6 +974,7 @@
973974
892FB4CE220402C0005293EC /* OverrideSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideSelectionController.swift; sourceTree = "<group>"; };
974975
895FE0942201234000FCF18A /* OverrideSelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverrideSelectionViewController.swift; sourceTree = "<group>"; };
975976
8968B1112408B3520074BB48 /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
977+
8968B113240C55F10074BB48 /* LoopSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopSettingsTests.swift; sourceTree = "<group>"; };
976978
898ECA5E218ABD17001E9D35 /* GlucoseChartScaler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseChartScaler.swift; sourceTree = "<group>"; };
977979
898ECA5F218ABD17001E9D35 /* GlucoseChartData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseChartData.swift; sourceTree = "<group>"; };
978980
898ECA62218ABD21001E9D35 /* ComplicationChartManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplicationChartManager.swift; sourceTree = "<group>"; };
@@ -1564,6 +1566,7 @@
15641566
43E2D90F1D20C581004DA55F /* Info.plist */,
15651567
7D2366E421250E0A0028B67D /* InfoPlist.strings */,
15661568
A9DAE7CF2332D77F006AE942 /* LoopTests.swift */,
1569+
8968B113240C55F10074BB48 /* LoopSettingsTests.swift */,
15671570
);
15681571
path = LoopTests;
15691572
sourceTree = "<group>";
@@ -2753,6 +2756,7 @@
27532756
isa = PBXSourcesBuildPhase;
27542757
buildActionMask = 2147483647;
27552758
files = (
2759+
8968B114240C55F10074BB48 /* LoopSettingsTests.swift in Sources */,
27562760
A9DAE7D02332D77F006AE942 /* LoopTests.swift in Sources */,
27572761
);
27582762
runOnlyForDeploymentPostprocessing = 0;

Loop/Managers/LoopDataManager.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ final class LoopDataManager {
153153
guard settings != oldValue else {
154154
return
155155
}
156+
157+
if settings.preMealOverride != oldValue.preMealOverride {
158+
// The prediction isn't actually invalid, but a target range change requires recomputing recommended doses
159+
predictedGlucose = nil
160+
}
161+
156162
if settings.scheduleOverride != oldValue.scheduleOverride {
157163
overrideHistory.recordOverride(settings.scheduleOverride)
158164

Loop/Managers/StatusChartsManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ extension StatusChartsManager {
7979
}
8080
}
8181

82+
var preMealOverride: TemporaryScheduleOverride? {
83+
get {
84+
return glucose.preMealOverride
85+
}
86+
set {
87+
glucose.preMealOverride = newValue
88+
invalidateChart(atIndex: ChartIndex.glucose.rawValue)
89+
}
90+
}
91+
8292
var scheduleOverride: TemporaryScheduleOverride? {
8393
get {
8494
return glucose.scheduleOverride

Loop/Managers/WatchDataManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ extension WatchDataManager: WCSessionDelegate {
250250
if let watchSettings = LoopSettingsUserInfo(rawValue: message)?.settings {
251251
// So far we only support watch changes of temporary schedule overrides
252252
var settings = deviceManager.loopManager.settings
253+
settings.preMealOverride = watchSettings.preMealOverride
253254
settings.scheduleOverride = watchSettings.scheduleOverride
254255

255256
// Prevent re-sending these updated settings back to the watch

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ final class StatusTableViewController: ChartsTableViewController {
439439
}
440440
if currentContext.contains(.targets) {
441441
self.statusCharts.targetGlucoseSchedule = self.deviceManager.loopManager.settings.glucoseTargetRangeSchedule
442+
self.statusCharts.preMealOverride = self.deviceManager.loopManager.settings.preMealOverride
442443
self.statusCharts.scheduleOverride = self.deviceManager.loopManager.settings.scheduleOverride
443444
}
444445
if self.statusCharts.scheduleOverride?.hasFinished() == true {

LoopCore/LoopSettings.swift

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,21 @@ public struct LoopSettings: Equatable {
2323

2424
public var overridePresets: [TemporaryScheduleOverridePreset] = []
2525

26-
public var scheduleOverride: TemporaryScheduleOverride?
26+
public var scheduleOverride: TemporaryScheduleOverride? {
27+
didSet {
28+
if let newValue = scheduleOverride, newValue.context == .preMeal {
29+
preconditionFailure("The `scheduleOverride` field should not be used for a pre-meal target range override; use `preMealOverride` instead")
30+
}
31+
}
32+
}
33+
34+
public var preMealOverride: TemporaryScheduleOverride? {
35+
didSet {
36+
if let newValue = preMealOverride, newValue.context != .preMeal || newValue.settings.insulinNeedsScaleFactor != nil {
37+
preconditionFailure("The `preMealOverride` field should be used only for a pre-meal target range override")
38+
}
39+
}
40+
}
2741

2842
public var maximumBasalRatePerHour: Double?
2943

@@ -106,26 +120,37 @@ public struct LoopSettings: Equatable {
106120

107121
extension LoopSettings {
108122
public var glucoseTargetRangeScheduleApplyingOverrideIfActive: GlucoseRangeSchedule? {
109-
if let override = scheduleOverride, override.isActive() {
110-
return glucoseTargetRangeSchedule?.applyingOverride(override)
123+
let currentEffectiveOverride: TemporaryScheduleOverride?
124+
switch (preMealOverride, scheduleOverride) {
125+
case (let preMealOverride?, nil):
126+
currentEffectiveOverride = preMealOverride
127+
case (nil, let scheduleOverride?):
128+
currentEffectiveOverride = scheduleOverride
129+
case (let preMealOverride?, let scheduleOverride?):
130+
currentEffectiveOverride = preMealOverride.endDate > Date()
131+
? preMealOverride
132+
: scheduleOverride
133+
case (nil, nil):
134+
currentEffectiveOverride = nil
135+
}
136+
137+
if let effectiveOverride = currentEffectiveOverride {
138+
return glucoseTargetRangeSchedule?.applyingOverride(effectiveOverride)
111139
} else {
112140
return glucoseTargetRangeSchedule
113141
}
114142
}
115143

116144
public func scheduleOverrideEnabled(at date: Date = Date()) -> Bool {
117-
guard let override = scheduleOverride else { return false }
118-
return override.isActive(at: date)
145+
return scheduleOverride?.isActive(at: date) == true
119146
}
120147

121148
public func nonPreMealOverrideEnabled(at date: Date = Date()) -> Bool {
122-
guard let override = scheduleOverride else { return false }
123-
return override.context != .preMeal && override.isActive(at: date)
149+
return scheduleOverride?.isActive(at: date) == true
124150
}
125151

126152
public func preMealTargetEnabled(at date: Date = Date()) -> Bool {
127-
guard let override = scheduleOverride else { return false }
128-
return override.context == .preMeal && override.isActive(at: date)
153+
return preMealOverride?.isActive(at: date) == true
129154
}
130155

131156
public func futureOverrideEnabled(relativeTo date: Date = Date()) -> Bool {
@@ -134,16 +159,16 @@ extension LoopSettings {
134159
}
135160

136161
public mutating func enablePreMealOverride(at date: Date = Date(), for duration: TimeInterval) {
137-
scheduleOverride = preMealOverride(beginningAt: date, for: duration)
162+
preMealOverride = makePreMealOverride(beginningAt: date, for: duration)
138163
}
139164

140-
public func preMealOverride(beginningAt date: Date = Date(), for duration: TimeInterval) -> TemporaryScheduleOverride? {
141-
guard let premealTargetRange = preMealTargetRange, let unit = glucoseUnit else {
165+
private func makePreMealOverride(beginningAt date: Date = Date(), for duration: TimeInterval) -> TemporaryScheduleOverride? {
166+
guard let preMealTargetRange = preMealTargetRange, let unit = glucoseUnit else {
142167
return nil
143168
}
144169
return TemporaryScheduleOverride(
145170
context: .preMeal,
146-
settings: TemporaryScheduleOverrideSettings(unit: unit, targetRange: premealTargetRange),
171+
settings: TemporaryScheduleOverrideSettings(unit: unit, targetRange: preMealTargetRange),
147172
startDate: date,
148173
duration: .finite(duration),
149174
enactTrigger: .local,
@@ -153,6 +178,7 @@ extension LoopSettings {
153178

154179
public mutating func enableLegacyWorkoutOverride(at date: Date = Date(), for duration: TimeInterval) {
155180
scheduleOverride = legacyWorkoutOverride(beginningAt: date, for: duration)
181+
preMealOverride = nil
156182
}
157183

158184
public func legacyWorkoutOverride(beginningAt date: Date = Date(), for duration: TimeInterval) -> TemporaryScheduleOverride? {
@@ -170,6 +196,11 @@ extension LoopSettings {
170196
}
171197

172198
public mutating func clearOverride(matching context: TemporaryScheduleOverride.Context? = nil) {
199+
if context == .preMeal {
200+
preMealOverride = nil
201+
return
202+
}
203+
173204
guard let override = scheduleOverride else { return }
174205
if let context = context {
175206
if override.context == context {
@@ -223,6 +254,10 @@ extension LoopSettings: RawRepresentable {
223254
self.overridePresets = rawPresets.compactMap(TemporaryScheduleOverridePreset.init(rawValue:))
224255
}
225256

257+
if let rawPreMealOverride = rawValue["preMealOverride"] as? TemporaryScheduleOverride.RawValue {
258+
self.preMealOverride = TemporaryScheduleOverride(rawValue: rawPreMealOverride)
259+
}
260+
226261
if let rawOverride = rawValue["scheduleOverride"] as? TemporaryScheduleOverride.RawValue {
227262
self.scheduleOverride = TemporaryScheduleOverride(rawValue: rawOverride)
228263
}
@@ -246,6 +281,7 @@ extension LoopSettings: RawRepresentable {
246281
raw["glucoseTargetRangeSchedule"] = glucoseTargetRangeSchedule?.rawValue
247282
raw["preMealTargetRange"] = preMealTargetRange?.rawValue
248283
raw["legacyWorkoutTargetRange"] = legacyWorkoutTargetRange?.rawValue
284+
raw["preMealOverride"] = preMealOverride?.rawValue
249285
raw["scheduleOverride"] = scheduleOverride?.rawValue
250286
raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour
251287
raw["maximumBolus"] = maximumBolus

LoopTests/LoopSettingsTests.swift

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// LoopSettingsTests.swift
3+
// LoopTests
4+
//
5+
// Created by Michael Pangburn on 3/1/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import XCTest
10+
import LoopCore
11+
import LoopKit
12+
13+
14+
class LoopSettingsTests: XCTestCase {
15+
private let preMealRange = DoubleRange(minValue: 80, maxValue: 80)
16+
17+
private lazy var settings: LoopSettings = {
18+
var settings = LoopSettings()
19+
settings.preMealTargetRange = preMealRange
20+
settings.glucoseTargetRangeSchedule = GlucoseRangeSchedule(
21+
unit: .milligramsPerDeciliter,
22+
dailyItems: [.init(startTime: 0, value: DoubleRange(minValue: 95, maxValue: 105))]
23+
)
24+
return settings
25+
}()
26+
27+
func testPreMealOverride() {
28+
var settings = self.settings
29+
let preMealStart = Date()
30+
settings.enablePreMealOverride(at: preMealStart, for: 1 /* hour */ * 60 * 60)
31+
let actualPreMealRange = settings.glucoseTargetRangeScheduleApplyingOverrideIfActive?.value(at: preMealStart.addingTimeInterval(30 /* minutes */ * 60))
32+
XCTAssertEqual(actualPreMealRange, preMealRange)
33+
}
34+
35+
func testScheduleOverride() {
36+
var settings = self.settings
37+
let overrideStart = Date()
38+
let overrideTargetRange = DoubleRange(minValue: 130, maxValue: 150)
39+
let override = TemporaryScheduleOverride(
40+
context: .custom,
41+
settings: TemporaryScheduleOverrideSettings(
42+
unit: .milligramsPerDeciliter,
43+
targetRange: overrideTargetRange
44+
),
45+
startDate: overrideStart,
46+
duration: .finite(3 /* hours */ * 60 * 60),
47+
enactTrigger: .local,
48+
syncIdentifier: UUID()
49+
)
50+
settings.scheduleOverride = override
51+
let actualOverrideRange = settings.glucoseTargetRangeScheduleApplyingOverrideIfActive?.value(at: overrideStart.addingTimeInterval(30 /* minutes */ * 60))
52+
XCTAssertEqual(actualOverrideRange, overrideTargetRange)
53+
}
54+
55+
func testBothPreMealAndScheduleOverride() {
56+
var settings = self.settings
57+
let preMealStart = Date()
58+
settings.enablePreMealOverride(at: preMealStart, for: 1 /* hour */ * 60 * 60)
59+
60+
let overrideStart = Date()
61+
let overrideTargetRange = DoubleRange(minValue: 130, maxValue: 150)
62+
let override = TemporaryScheduleOverride(
63+
context: .custom,
64+
settings: TemporaryScheduleOverrideSettings(
65+
unit: .milligramsPerDeciliter,
66+
targetRange: overrideTargetRange
67+
),
68+
startDate: overrideStart,
69+
duration: .finite(3 /* hours */ * 60 * 60),
70+
enactTrigger: .local,
71+
syncIdentifier: UUID()
72+
)
73+
settings.scheduleOverride = override
74+
75+
let actualPreMealRange = settings.glucoseTargetRangeScheduleApplyingOverrideIfActive?.value(at: preMealStart.addingTimeInterval(30 /* minutes */ * 60))
76+
XCTAssertEqual(actualPreMealRange, preMealRange)
77+
78+
// The pre-meal range should be projected into the future, despite the simultaneous schedule override
79+
let preMealRangeDuringOverride = settings.glucoseTargetRangeScheduleApplyingOverrideIfActive?.value(at: preMealStart.addingTimeInterval(2 /* hours */ * 60 * 60))
80+
XCTAssertEqual(preMealRangeDuringOverride, preMealRange)
81+
}
82+
83+
func testScheduleOverrideWithExpiredPreMealOverride() {
84+
var settings = self.settings
85+
settings.preMealOverride = TemporaryScheduleOverride(
86+
context: .preMeal,
87+
settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, targetRange: preMealRange),
88+
startDate: Date(timeIntervalSinceNow: -2 /* hours */ * 60 * 60),
89+
duration: .finite(1 /* hours */ * 60 * 60),
90+
enactTrigger: .local,
91+
syncIdentifier: UUID()
92+
)
93+
94+
let overrideStart = Date()
95+
let overrideTargetRange = DoubleRange(minValue: 130, maxValue: 150)
96+
let override = TemporaryScheduleOverride(
97+
context: .custom,
98+
settings: TemporaryScheduleOverrideSettings(
99+
unit: .milligramsPerDeciliter,
100+
targetRange: overrideTargetRange
101+
),
102+
startDate: overrideStart,
103+
duration: .finite(3 /* hours */ * 60 * 60),
104+
enactTrigger: .local,
105+
syncIdentifier: UUID()
106+
)
107+
settings.scheduleOverride = override
108+
109+
let actualOverrideRange = settings.glucoseTargetRangeScheduleApplyingOverrideIfActive?.value(at: overrideStart.addingTimeInterval(2 /* hours */ * 60 * 60))
110+
XCTAssertEqual(actualOverrideRange, overrideTargetRange)
111+
}
112+
}

0 commit comments

Comments
 (0)