Skip to content

Commit 896aaf9

Browse files
authored
Merge pull request #65 from florianpreknya/BasicStats
Daily stats feature
2 parents 3d86932 + bc13d5d commit 896aaf9

25 files changed

Lines changed: 2107 additions & 234 deletions

nightguard WatchKit Extension/ChartPainter.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ class ChartPainter {
301301
}
302302

303303
fileprivate func durationIsMoreThan6Hours(_ minTimestamp : Double, maxTimestamp : Double) -> Bool {
304-
return maxTimestamp - minTimestamp > 6 * 60 * 60 * 1000
304+
let sixHours = Double(6 * 60 * 60 * 1000)
305+
return (maxTimestamp - minTimestamp) > sixHours
305306
}
306307

307308
fileprivate func paintEverySecondHour(_ context : CGContext, attrs : [NSAttributedStringKey : Any]) {

nightguard WatchKit Extension/UserDefaultsRepository.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ class UserDefaultsRepository {
100100
static let volumeKeysOnAlertSnoozeOption = UserDefaultsValue<QuickSnoozeOption>(key: "volumeKeysOnAlertSnoozeOption", default: .doNothing)
101101
#endif
102102

103+
// show/hide stats
104+
static let showStats = UserDefaultsValue<Bool>(key: "showStats", default: true)
105+
103106
/* Parses the URI entered in the UI and extracts the token if one is present. */
104107
fileprivate static func parseBaseUri() {
105108
url = nil

nightguard.xcodeproj/project.pbxproj

Lines changed: 78 additions & 10 deletions
Large diffs are not rendered by default.

nightguard/A1cView.swift

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// A1cView.swift
3+
// nightguard
4+
//
5+
// Created by Florian Preknya on 3/18/19.
6+
// Copyright © 2019 private. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
extension UIColor {
12+
13+
// Creates a color-meter like UIColor by placing the given value in correspondence with its good-bad range, resulting a green color if the value is very good on that scale, a red color if is very bad or yellow & compositions with red-green if is in between
14+
static func redYellowGreen(for value: CGFloat, bestValue: CGFloat, worstValue: CGFloat) -> UIColor {
15+
if bestValue < worstValue {
16+
let power = max(min((worstValue - CGFloat(value)) / (worstValue - bestValue), 1), 0)
17+
return UIColor(red: 1 - power, green: power, blue: 0, alpha: 1)
18+
} else {
19+
let power = max(min((bestValue - CGFloat(value)) / (bestValue - worstValue), 1), 0)
20+
return UIColor(red: power, green: 1 - power, blue: 0, alpha: 1)
21+
}
22+
}
23+
}
24+
25+
/**
26+
A stats view that displays the A1c value, the average BG value, standard deviation & variation values. It appreciates the values by giving a colored feedback to user: green - good values, yellow - okish, red - pretty bad.
27+
*/
28+
class A1cView: BasicStatsControl {
29+
30+
override func createPages() -> [StatsPage] {
31+
return [
32+
StatsPage(name: "A1c", formattedValue: model?.formattedA1c),
33+
StatsPage(name: "Average", formattedValue: model?.formattedAverageGlucose?.replacingOccurrences(of: " ", with: "\n")),
34+
StatsPage(name: "Std Deviation", formattedValue: model?.formattedStandardDeviation?.replacingOccurrences(of: " ", with: "\n")),
35+
StatsPage(name: "Coefficient of Variation", formattedValue: model?.formattedCoefficientOfVariation)
36+
]
37+
}
38+
39+
fileprivate var a1cColor: UIColor? {
40+
41+
guard let a1c = model?.a1c else {
42+
return nil
43+
}
44+
45+
return UIColor.redYellowGreen(for: CGFloat(a1c), bestValue: 5.5, worstValue: 8.5)
46+
}
47+
48+
fileprivate var variationColor: UIColor? {
49+
50+
guard let coefficientOfVariation = model?.coefficientOfVariation else {
51+
return nil
52+
}
53+
54+
return UIColor.redYellowGreen(for: CGFloat(coefficientOfVariation), bestValue: 0.3, worstValue: 0.5)
55+
}
56+
57+
fileprivate var modelColor: UIColor? {
58+
return (currentPageIndex < 2) ? a1cColor : variationColor
59+
}
60+
61+
override func commonInit() {
62+
super.commonInit()
63+
64+
diagramView.dataSource = self
65+
// diagramView.separatorWidh = 8
66+
// diagramView.separatorColor = .black
67+
// diagramView.startAngle = .pi * 0.75
68+
// diagramView.endAngle = 2 * .pi + .pi * 0.75
69+
}
70+
71+
override func modelWasSet() {
72+
super.modelWasSet()
73+
diagramView.backgroundColor = modelColor?.withAlphaComponent(0.1)
74+
}
75+
76+
override func pageChanged() {
77+
super.pageChanged()
78+
diagramView.backgroundColor = modelColor?.withAlphaComponent(0.1)
79+
}
80+
}
81+
82+
extension A1cView: SMDiagramViewDataSource {
83+
84+
@objc func numberOfSegmentsIn(diagramView: SMDiagramView) -> Int {
85+
return 2
86+
}
87+
88+
func diagramView(_ diagramView: SMDiagramView, colorForSegmentAtIndex index: NSInteger, angle: CGFloat) -> UIColor? {
89+
// return (index == 1) ? a1cColor : variationColor
90+
return modelColor
91+
}
92+
93+
func diagramView(_ diagramView: SMDiagramView, radiusForSegmentAtIndex index: NSInteger, proportion: CGFloat, angle: CGFloat) -> CGFloat {
94+
return (diagramView.frame.size.height - 2/*diagramView.arcWidth*/) / 2
95+
}
96+
97+
func diagramView(_ diagramView: SMDiagramView, lineWidthForSegmentAtIndex index: NSInteger, angle: CGFloat) -> CGFloat {
98+
//not called for SMDiagramViewModeSegment
99+
return 2.0
100+
}
101+
}

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 lastReadings = bloodValues.lastXMinutes(persistentHighMinutes.value)
151+
152+
// we should have at least a reading in 10 minutes for considering a persistent high
153+
if !lastReadings.isEmpty && (lastReadings.count >= (persistentHighMinutes.value / 10)) {
154+
if lastReadings.allSatisfy({ AlarmRule.isTooHigh($0.value) }) {
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)

0 commit comments

Comments
 (0)