Skip to content

Commit a6f226e

Browse files
Persistent high alert
1 parent f1f7a28 commit a6f226e

5 files changed

Lines changed: 163 additions & 32 deletions

File tree

nightguard.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
D16C8EDD22106C9300192117 /* QuickSnoozeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C8EDC22106C9300192117 /* QuickSnoozeOption.swift */; };
154154
D16C8EDF2211047300192117 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C8EDE2211047300192117 /* UIViewController+Extensions.swift */; };
155155
D16C8EE22213022E00192117 /* UserInteractionDetectorWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16C8EE12213022E00192117 /* UserInteractionDetectorWindow.swift */; };
156+
D17F7938224D3C500074907B /* PersistentHighViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17F7937224D3C500074907B /* PersistentHighViewController.swift */; };
156157
D18C5D8A22293C220099D96E /* BasicStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18C5D8922293C220099D96E /* BasicStats.swift */; };
157158
D18C5D8B22293C230099D96E /* BasicStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18C5D8922293C220099D96E /* BasicStats.swift */; };
158159
D18C5D8C22293C230099D96E /* BasicStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18C5D8922293C220099D96E /* BasicStats.swift */; };
@@ -380,6 +381,7 @@
380381
D16C8EDC22106C9300192117 /* QuickSnoozeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSnoozeOption.swift; sourceTree = "<group>"; };
381382
D16C8EDE2211047300192117 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = "<group>"; };
382383
D16C8EE12213022E00192117 /* UserInteractionDetectorWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInteractionDetectorWindow.swift; sourceTree = "<group>"; };
384+
D17F7937224D3C500074907B /* PersistentHighViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentHighViewController.swift; sourceTree = "<group>"; };
383385
D18C5D8922293C220099D96E /* BasicStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicStats.swift; sourceTree = "<group>"; };
384386
D1AEFED721FE00A200821DF6 /* AnyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyConvertible.swift; sourceTree = "<group>"; };
385387
D1B777E32243E27B003FEDF0 /* TouchReportingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchReportingView.swift; sourceTree = "<group>"; };
@@ -751,6 +753,7 @@
751753
D1EBB6A82223FF0200CE27FF /* MissedReadingsViewController.swift */,
752754
D160D091220E4248002B4633 /* AlertVolumeViewController.swift */,
753755
D16C8ED62210377000192117 /* SnoozeActionsViewController.swift */,
756+
D17F7937224D3C500074907B /* PersistentHighViewController.swift */,
754757
);
755758
name = forms;
756759
sourceTree = "<group>";
@@ -1273,6 +1276,7 @@
12731276
D1B777E42243E27B003FEDF0 /* TouchReportingView.swift in Sources */,
12741277
D18C5D8A22293C220099D96E /* BasicStats.swift in Sources */,
12751278
43F1E0EE1D07693300C329A2 /* AlarmRule.swift in Sources */,
1279+
D17F7938224D3C500074907B /* PersistentHighViewController.swift in Sources */,
12761280
432E62EF1D17359600DD7978 /* StringExtension.swift in Sources */,
12771281
D1D1623E221AAC05006F990A /* WatchSyncRequestMessage.swift in Sources */,
12781282
43F1E1011D07698000C329A2 /* AppDelegate.swift in Sources */,

nightguard/AlarmRule.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,19 @@ class AlarmRule {
6262
static var isSmartSnoozeEnabled = UserDefaultsValue<Bool>(key: "smartSnoozeEnabled", default: true)
6363
.group(UserDefaultsValueGroups.GroupNames.watchSync)
6464
.group(UserDefaultsValueGroups.GroupNames.alarm)
65+
66+
static var isPersistentHighEnabled = UserDefaultsValue<Bool>(key: "persistentHighEnabled", default: false)
67+
.group(UserDefaultsValueGroups.GroupNames.watchSync)
68+
.group(UserDefaultsValueGroups.GroupNames.alarm)
6569

70+
static var persistentHighMinutes = UserDefaultsValue<Int>(key: "persistentHighMinutes", default: 30)
71+
.group(UserDefaultsValueGroups.GroupNames.watchSync)
72+
.group(UserDefaultsValueGroups.GroupNames.alarm)
73+
74+
static var persistentHighUpperBound = UserDefaultsValue<Float>(key: "persistentHighUpperBound", default: 250)
75+
.group(UserDefaultsValueGroups.GroupNames.watchSync)
76+
.group(UserDefaultsValueGroups.GroupNames.alarm)
77+
6678
/*
6779
* Returns true if the alarm should be played.
6880
* Snooze is true if the Alarm has been manually deactivated.
@@ -130,6 +142,24 @@ class AlarmRule {
130142
}
131143

132144
if isTooHigh {
145+
146+
if isPersistentHighEnabled.value {
147+
if currentReading.value < persistentHighUpperBound.value {
148+
149+
// if all the previous readings (for the defined minutes are high, we'll consider it a persistent high)
150+
let lastReadingValues = bloodValues.lastXMinutes(persistentHighMinutes.value).map { Double($0.value) }
151+
152+
// we should have at least a reading in 10 minutes for considering a persistent high
153+
if !lastReadingValues.isEmpty && (lastReadingValues.count >= (persistentHighMinutes.value / 10)) {
154+
if AlarmRule.isTooHigh(Float(lastReadingValues.average)) {
155+
return "Persistent High BG"
156+
} else {
157+
return nil
158+
}
159+
}
160+
}
161+
}
162+
133163
return "High BG"
134164
} else if isTooLow {
135165
return "Low BG"

nightguard/AlarmViewController.swift

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ class AlarmViewController: CustomFormViewController {
2020
fileprivate let MAX_ALERT_BELOW_VALUE : Float = 200
2121
fileprivate let MIN_ALERT_BELOW_VALUE : Float = 50
2222

23-
fileprivate let SNAP_INCREMENT : Float = 10 // or change it to 5?
24-
2523
override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
2624
return UIInterfaceOrientationMask.portrait
2725
}
@@ -39,10 +37,10 @@ class AlarmViewController: CustomFormViewController {
3937

4038
override func constructForm() {
4139

42-
aboveSliderRow = createSliderRow(initialValue: AlarmRule.alertIfAboveValue.value, minimumValue: MIN_ALERT_ABOVE_VALUE, maximumValue: MAX_ALERT_ABOVE_VALUE)
40+
aboveSliderRow = SliderRow.glucoseLevelSlider(initialValue: AlarmRule.alertIfAboveValue.value, minimumValue: MIN_ALERT_ABOVE_VALUE, maximumValue: MAX_ALERT_ABOVE_VALUE)
4341
aboveSliderRow.cell.slider.addTarget(self, action: #selector(onSliderValueChanged(slider:event:)), for: .valueChanged)
4442

45-
belowSliderRow = createSliderRow(initialValue: AlarmRule.alertIfBelowValue.value, minimumValue: MIN_ALERT_BELOW_VALUE, maximumValue: MAX_ALERT_BELOW_VALUE)
43+
belowSliderRow = SliderRow.glucoseLevelSlider(initialValue: AlarmRule.alertIfBelowValue.value, minimumValue: MIN_ALERT_BELOW_VALUE, maximumValue: MAX_ALERT_BELOW_VALUE)
4644
belowSliderRow.cell.slider.addTarget(self, action: #selector(onSliderValueChanged(slider:event:)), for: .valueChanged)
4745

4846

@@ -89,6 +87,28 @@ class AlarmViewController: CustomFormViewController {
8987
}
9088
}
9189

90+
<<< ButtonRowWithDynamicDetails("Persistent High") { row in
91+
row.controllerProvider = { return PersistentHighViewController() }
92+
row.detailTextProvider = {
93+
94+
let urgentHighInMgdl = AlarmRule.persistentHighUpperBound.value
95+
let urgentHigh = UnitsConverter.toDisplayUnits("\(urgentHighInMgdl)")
96+
let units = UserDefaultsRepository.units.value.description
97+
let urgentHighWithUnits = "\(urgentHigh) \(units)"
98+
99+
if AlarmRule.isPersistentHighEnabled.value {
100+
if #available(iOS 11.0, *) {
101+
return "Alerts when BG remains high for more than \(AlarmRule.persistentHighMinutes.value) minutes or exceeds the urgent high value (\(urgentHighWithUnits))."
102+
} else {
103+
// single line, as iOS 10 doesn't expand cell for more lines
104+
return "\(AlarmRule.persistentHighMinutes.value) minutes (< \(urgentHighWithUnits))"
105+
}
106+
} else {
107+
return "Off"
108+
}
109+
}
110+
}
111+
92112
<<< ButtonRowWithDynamicDetails("Low Prediction") { row in
93113
row.controllerProvider = { return LowPredictionViewController() }
94114
row.detailTextProvider = {
@@ -174,34 +194,6 @@ class AlarmViewController: CustomFormViewController {
174194
}
175195
}
176196

177-
private func createSliderRow(initialValue: Float, minimumValue: Float, maximumValue: Float) -> SliderRow {
178-
179-
return SliderRow() { row in
180-
row.value = Float(UnitsConverter.toDisplayUnits("\(initialValue)"))!
181-
}.cellSetup { [weak self] cell, row in
182-
guard let self = self else { return }
183-
// row.shouldHideValue = true
184-
185-
let minimumValue = Float(UnitsConverter.toDisplayUnits("\(minimumValue)"))!
186-
let maximumValue = Float(UnitsConverter.toDisplayUnits("\(maximumValue)"))!
187-
let snapIncrement = (UserDefaultsRepository.units.value == .mgdl) ? self.SNAP_INCREMENT : 0.1
188-
189-
let steps = (maximumValue - minimumValue) / snapIncrement
190-
row.steps = UInt(steps.rounded())
191-
cell.slider.minimumValue = minimumValue
192-
cell.slider.maximumValue = maximumValue
193-
row.displayValueFor = { value in
194-
guard let value = value else { return "" }
195-
let units = UserDefaultsRepository.units.value.description
196-
return String("\(value.cleanValue) \(units)")
197-
}
198-
199-
// fixed width for value label
200-
let widthConstraint = NSLayoutConstraint(item: cell.valueLabel, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 96)
201-
cell.valueLabel.addConstraints([widthConstraint])
202-
}
203-
}
204-
205197
private func alertInvalidChange(message: String) {
206198
let alertController = UIAlertController(title: "Invalid change", message: message, preferredStyle: .alert)
207199
let actionOk = UIAlertAction(title: "OK", style: .default, handler: nil)

nightguard/CustomFormViewController.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,32 @@ extension SelectorViewController {
222222
}
223223
}
224224
}
225+
226+
extension SliderRow {
227+
228+
class func glucoseLevelSlider(initialValue: Float, minimumValue: Float, maximumValue: Float, snapIncrementForMgDl: Float = 10.0) -> SliderRow {
229+
230+
return SliderRow() { row in
231+
row.value = Float(UnitsConverter.toDisplayUnits("\(initialValue)"))!
232+
}.cellSetup { cell, row in
233+
234+
let minimumValue = Float(UnitsConverter.toDisplayUnits("\(minimumValue)"))!
235+
let maximumValue = Float(UnitsConverter.toDisplayUnits("\(maximumValue)"))!
236+
let snapIncrement = (UserDefaultsRepository.units.value == .mgdl) ? snapIncrementForMgDl : 0.1
237+
238+
let steps = (maximumValue - minimumValue) / snapIncrement
239+
row.steps = UInt(steps.rounded())
240+
cell.slider.minimumValue = minimumValue
241+
cell.slider.maximumValue = maximumValue
242+
row.displayValueFor = { value in
243+
guard let value = value else { return "" }
244+
let units = UserDefaultsRepository.units.value.description
245+
return String("\(value.cleanValue) \(units)")
246+
}
247+
248+
// fixed width for value label
249+
let widthConstraint = NSLayoutConstraint(item: cell.valueLabel, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 96)
250+
cell.valueLabel.addConstraints([widthConstraint])
251+
}
252+
}
253+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// PersistentHighViewController.swift
3+
// nightguard
4+
//
5+
// Created by Florian Preknya on 3/28/19.
6+
// Copyright © 2019 private. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import Eureka
11+
12+
class PersistentHighViewController: CustomFormViewController {
13+
14+
var urgentHighSliderRow: SliderRow!
15+
16+
fileprivate let alarmOptions = [15, 20, 30, 45, 60, 90, 120]
17+
private var selectableSection: SelectableSection<ListCheckRow<Int>>!
18+
19+
override func constructForm() {
20+
21+
selectableSection = SelectableSection<ListCheckRow<Int>>("Alert when high BG for more than", selectionType: .singleSelection(enableDeselection: true))
22+
selectableSection.onSelectSelectableRow = { cell, row in
23+
guard let value = row.value else { return }
24+
AlarmRule.persistentHighMinutes.value = value
25+
}
26+
selectableSection.hidden = "$PersistentHighSwitch == false"
27+
28+
for option in alarmOptions {
29+
selectableSection <<< ListCheckRow<Int>("\(option) Minutes") { lrow in
30+
lrow.title = "\(option) Minutes"
31+
lrow.selectableValue = option
32+
lrow.value = (option == AlarmRule.persistentHighMinutes.value) ? option : nil
33+
}
34+
}
35+
36+
urgentHighSliderRow = SliderRow.glucoseLevelSlider(initialValue: AlarmRule.persistentHighUpperBound.value, minimumValue: AlarmRule.alertIfAboveValue.value, maximumValue: 300)
37+
urgentHighSliderRow.cell.slider.addTarget(self, action: #selector(onSliderValueChanged(slider:event:)), for: .valueChanged)
38+
39+
let urgentHighSection = Section(header: "Urgent High", footer: "Alerts anytime when the blood glucose raises above this value.")
40+
urgentHighSection <<< urgentHighSliderRow
41+
urgentHighSection.hidden = "$PersistentHighSwitch == false"
42+
43+
44+
form +++ Section(header: "", footer: "Alerts when the BG remains high for a longer period. When on, this alert will delay the high BG alert until the period elapsed or until reaching a maximum BG level (urgent high).")
45+
<<< SwitchRow("PersistentHighSwitch") { row in
46+
row.title = "Persistent High"
47+
row.value = AlarmRule.isPersistentHighEnabled.value
48+
}.onChange { row in
49+
guard let value = row.value else { return }
50+
AlarmRule.isPersistentHighEnabled.value = value
51+
}
52+
53+
+++ selectableSection
54+
+++ urgentHighSection
55+
}
56+
57+
@objc func onSliderValueChanged(slider: UISlider, event: UIEvent) {
58+
guard let touchEvent = event.allTouches?.first else { return }
59+
60+
// modify UserDefaultsValue ONLY when slider value change events ended
61+
switch touchEvent.phase {
62+
case .ended:
63+
if slider === urgentHighSliderRow.cell.slider {
64+
65+
guard let value = urgentHighSliderRow.value else { return }
66+
let mgdlValue = UnitsConverter.toMgdl(value)
67+
68+
print("Changed (persistent) urgent high slider to \(mgdlValue) \(UserDefaultsRepository.units.value.description)")
69+
AlarmRule.persistentHighUpperBound.value = mgdlValue
70+
}
71+
72+
default:
73+
break
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)