Skip to content

Commit 4b97e38

Browse files
authored
Merge pull request #451 from tidepool-org/release/v1.1.0
Merge Release/v1.1.0 into dev
2 parents 8e7e26f + 192bcff commit 4b97e38

13 files changed

+418
-151
lines changed

Loop/Managers/Alerts/InAppModalAlertIssuer.swift

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class InAppModalAlertIssuer: AlertIssuer {
1414
private weak var alertPresenter: AlertPresenter?
1515
private weak var alertManagerResponder: AlertManagerResponder?
1616

17-
private var alertsShowing: [Alert.Identifier: (UIAlertController, Alert)] = [:]
17+
private var alertsPresented: [Alert.Identifier: (UIAlertController, Alert)] = [:]
1818
private var alertsPending: [Alert.Identifier: (Timer, Alert)] = [:]
1919

2020
typealias ActionFactoryFunction = (String?, UIAlertAction.Style, ((UIAlertAction) -> Void)?) -> UIAlertAction
@@ -29,7 +29,8 @@ public class InAppModalAlertIssuer: AlertIssuer {
2929
alertManagerResponder: AlertManagerResponder,
3030
soundPlayer: AlertSoundPlayer = DeviceAVSoundPlayer(),
3131
newActionFunc: @escaping ActionFactoryFunction = UIAlertAction.init,
32-
newTimerFunc: TimerFactoryFunction? = nil) {
32+
newTimerFunc: TimerFactoryFunction? = nil)
33+
{
3334
self.alertPresenter = alertPresenter
3435
self.alertManagerResponder = alertManagerResponder
3536
self.soundPlayer = soundPlayer
@@ -38,7 +39,7 @@ public class InAppModalAlertIssuer: AlertIssuer {
3839
return Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: repeats) { _ in block?() }
3940
}
4041
}
41-
42+
4243
public func issueAlert(_ alert: Alert) {
4344
switch alert.trigger {
4445
case .immediate:
@@ -52,21 +53,30 @@ public class InAppModalAlertIssuer: AlertIssuer {
5253

5354
public func retractAlert(identifier: Alert.Identifier) {
5455
DispatchQueue.main.async {
55-
self.alertsPending[identifier]?.0.invalidate()
56-
self.clearPendingAlert(identifier: identifier)
57-
self.removeDeliveredAlert(identifier: identifier, completion: nil)
56+
self.removePendingAlert(identifier: identifier)
57+
self.removePresentedAlert(identifier: identifier)
5858
}
5959
}
60-
61-
func removeDeliveredAlert(identifier: Alert.Identifier, completion: (() -> Void)?) {
62-
self.alertsShowing[identifier]?.0.dismiss(animated: true, completion: completion)
63-
self.clearDeliveredAlert(identifier: identifier)
60+
61+
func removePresentedAlert(identifier: Alert.Identifier, completion: (() -> Void)? = nil) {
62+
guard let alertPresented = alertsPresented[identifier] else {
63+
completion?()
64+
return
65+
}
66+
alertPresenter?.dismissAlert(alertPresented.0, animated: true, completion: completion)
67+
clearPresentedAlert(identifier: identifier)
68+
}
69+
70+
func removePendingAlert(identifier: Alert.Identifier) {
71+
guard let alertPending = alertsPending[identifier] else { return }
72+
alertPending.0.invalidate()
73+
clearPendingAlert(identifier: identifier)
6474
}
6575
}
6676

6777
/// Private functions
6878
extension InAppModalAlertIssuer {
69-
79+
7080
private func schedule(alert: Alert, interval: TimeInterval, repeats: Bool) {
7181
guard alert.foregroundContent != nil else {
7282
return
@@ -90,59 +100,60 @@ extension InAppModalAlertIssuer {
90100
return
91101
}
92102
DispatchQueue.main.async {
93-
if self.isAlertShowing(identifier: alert.identifier) {
103+
if self.isAlertPresented(identifier: alert.identifier) {
94104
return
95105
}
96106
let alertController = self.constructAlert(title: content.title,
97107
message: content.body,
98108
action: content.acknowledgeActionButtonLabel,
99109
isCritical: content.isCritical) { [weak self] in
100-
self?.clearDeliveredAlert(identifier: alert.identifier)
101-
self?.alertManagerResponder?.acknowledgeAlert(identifier: alert.identifier)
110+
// the completion is called after the alert is acknowledged
111+
self?.clearPresentedAlert(identifier: alert.identifier)
112+
self?.alertManagerResponder?.acknowledgeAlert(identifier: alert.identifier)
102113
}
103114
self.alertPresenter?.present(alertController, animated: true) { [weak self] in
104-
// the completion is called after the alert is displayed
115+
// the completion is called after the alert is presented
105116
self?.playSound(for: alert)
106-
self?.addDeliveredAlert(alert: alert, controller: alertController)
117+
self?.addPresentedAlert(alert: alert, controller: alertController)
107118
}
108119
}
109120
}
110121

111122
private func addPendingAlert(alert: Alert, timer: Timer) {
112123
dispatchPrecondition(condition: .onQueue(.main))
113-
self.alertsPending[alert.identifier] = (timer, alert)
124+
alertsPending[alert.identifier] = (timer, alert)
114125
}
115126

116-
private func addDeliveredAlert(alert: Alert, controller: UIAlertController) {
127+
private func addPresentedAlert(alert: Alert, controller: UIAlertController) {
117128
dispatchPrecondition(condition: .onQueue(.main))
118-
self.alertsShowing[alert.identifier] = (controller, alert)
129+
alertsPresented[alert.identifier] = (controller, alert)
119130
}
120131

121132
private func clearPendingAlert(identifier: Alert.Identifier) {
122133
dispatchPrecondition(condition: .onQueue(.main))
123134
alertsPending[identifier] = nil
124135
}
125136

126-
private func clearDeliveredAlert(identifier: Alert.Identifier) {
137+
private func clearPresentedAlert(identifier: Alert.Identifier) {
127138
dispatchPrecondition(condition: .onQueue(.main))
128-
alertsShowing[identifier] = nil
139+
alertsPresented[identifier] = nil
129140
}
130-
141+
131142
private func isAlertPending(identifier: Alert.Identifier) -> Bool {
132143
dispatchPrecondition(condition: .onQueue(.main))
133144
return alertsPending.index(forKey: identifier) != nil
134145
}
135146

136-
private func isAlertShowing(identifier: Alert.Identifier) -> Bool {
147+
private func isAlertPresented(identifier: Alert.Identifier) -> Bool {
137148
dispatchPrecondition(condition: .onQueue(.main))
138-
return alertsShowing.index(forKey: identifier) != nil
149+
return alertsPresented.index(forKey: identifier) != nil
139150
}
140151

141-
private func constructAlert(title: String, message: String, action: String, isCritical: Bool, completion: @escaping () -> Void) -> UIAlertController {
152+
private func constructAlert(title: String, message: String, action: String, isCritical: Bool, acknowledgeCompletion: @escaping () -> Void) -> UIAlertController {
142153
dispatchPrecondition(condition: .onQueue(.main))
143154
// For now, this is a simple alert with an "OK" button
144155
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
145-
alertController.addAction(newActionFunc(action, .default, { _ in completion() }))
156+
alertController.addAction(newActionFunc(action, .default, { _ in acknowledgeCompletion() }))
146157
return alertController
147158
}
148159

Loop/Managers/DeliveryUncertaintyAlertManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class DeliveryUncertaintyAlertManager {
3939
self.showUncertainDeliveryRecoveryView()
4040
}
4141
alert.addAction(action)
42-
self.alertPresenter.dismiss(animated: false) {
42+
self.alertPresenter.dismissTopMost(animated: false) {
4343
self.alertPresenter.present(alert, animated: animated)
4444
}
4545
self.uncertainDeliveryAlert = alert

Loop/Managers/LoopAppManager.swift

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,20 @@ public protocol AlertPresenter: AnyObject {
2626
/// - Parameters:
2727
/// - animated: Animate the alert view controller dismissal or not.
2828
/// - completion: Completion to call once view controller is dismissed.
29-
func dismiss(animated: Bool, completion: (() -> Void)?)
29+
func dismissTopMost(animated: Bool, completion: (() -> Void)?)
30+
31+
/// Dismiss an alert, even if it is not the top most alert.
32+
/// - Parameters:
33+
/// - alertToDismiss: The alert to dismiss
34+
/// - animated: Animate the alert view controller dismissal or not.
35+
/// - completion: Completion to call once view controller is dismissed.
36+
func dismissAlert(_ alertToDismiss: UIAlertController, animated: Bool, completion: (() -> Void)?)
3037
}
3138

3239
public extension AlertPresenter {
3340
func present(_ viewController: UIViewController, animated: Bool) { present(viewController, animated: animated, completion: nil) }
34-
func dismiss(animated: Bool) { dismiss(animated: animated, completion: nil) }
41+
func dismissTopMost(animated: Bool) { dismissTopMost(animated: animated, completion: nil) }
42+
func dismissAlert(_ alertToDismiss: UIAlertController, animated: Bool) { dismissAlert(alertToDismiss, animated: animated, completion: nil) }
3543
}
3644

3745
protocol WindowProvider: AnyObject {
@@ -322,9 +330,56 @@ extension LoopAppManager: AlertPresenter {
322330
rootViewController?.topmostViewController.present(viewControllerToPresent, animated: animated, completion: completion)
323331
}
324332

325-
func dismiss(animated: Bool, completion: (() -> Void)?) {
333+
func dismissTopMost(animated: Bool, completion: (() -> Void)?) {
326334
rootViewController?.topmostViewController.dismiss(animated: animated, completion: completion)
327335
}
336+
337+
func dismissAlert(_ alertToDismiss: UIAlertController, animated: Bool, completion: (() -> Void)?) {
338+
if rootViewController?.topmostViewController == alertToDismiss {
339+
dismissTopMost(animated: animated, completion: completion)
340+
} else {
341+
// check if the alert to dismiss is presenting another alert (and so on)
342+
// calling dismiss() on an alert presenting another alert will only dismiss the presented alert
343+
// (and any other alerts presented by the presented alert)
344+
345+
// get the stack of presented alerts that would be undesirably dismissed
346+
var presentedAlerts: [UIAlertController] = []
347+
var currentAlert = alertToDismiss
348+
while let presentedAlert = currentAlert.presentedViewController as? UIAlertController {
349+
presentedAlerts.append(presentedAlert)
350+
currentAlert = presentedAlert
351+
}
352+
353+
if presentedAlerts.isEmpty {
354+
alertToDismiss.dismiss(animated: animated, completion: completion)
355+
} else {
356+
// Do not animate any of these view transitions, since the alert to dismiss is not at the top of the stack
357+
358+
// dismiss all the child presented alerts.
359+
// Calling dismiss() on a VC that is presenting an other VC will dismiss the presented VC and all of its child presented VCs
360+
alertToDismiss.dismiss(animated: false) {
361+
// dismiss the desired alert
362+
// Calling dismiss() on a VC that is NOT presenting any other VCs will dismiss said VC
363+
alertToDismiss.dismiss(animated: false) {
364+
// present the child alerts that were undesirably dismissed
365+
var orderedPresentationBlock: (() -> Void)? = nil
366+
for alert in presentedAlerts.reversed() {
367+
if alert == presentedAlerts.last {
368+
orderedPresentationBlock = {
369+
self.present(alert, animated: false, completion: completion)
370+
}
371+
} else {
372+
orderedPresentationBlock = {
373+
self.present(alert, animated: false, completion: orderedPresentationBlock)
374+
}
375+
}
376+
}
377+
orderedPresentationBlock?()
378+
}
379+
}
380+
}
381+
}
382+
}
328383
}
329384

330385
// MARK: - DeviceOrientationController

Loop/Models/LoopConstants.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,9 @@ enum LoopConstants {
6161
static let missedMealWarningGlucoseRiseThreshold = 3.0 // mg/dL/m
6262
static let missedMealWarningGlucoseRecencyWindow = TimeInterval(minutes: 20)
6363
static let missedMealWarningVelocitySampleMinDuration = TimeInterval(minutes: 12)
64+
65+
// Bolus calculator warning thresholds
66+
static let simpleBolusCalculatorMinGlucoseBolusRecommendation = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 70)
67+
static let simpleBolusCalculatorMinGlucoseMealBolusRecommendation = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 55)
68+
static let simpleBolusCalculatorGlucoseWarningLimit = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 70)
6469
}

Loop/Models/SimpleBolusCalculator.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ struct SimpleBolusCalculator {
3535
recommendedBolus += correctionBolus
3636
}
3737
}
38+
39+
let recommendationLimit = mealCarbs != nil ? LoopConstants.simpleBolusCalculatorMinGlucoseMealBolusRecommendation : LoopConstants.simpleBolusCalculatorMinGlucoseBolusRecommendation
40+
41+
if manualGlucose < recommendationLimit {
42+
recommendedBolus = 0
43+
}
3844
}
3945

4046
// No negative recommendation

Loop/View Controllers/CarbAbsorptionViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,8 @@ final class CarbAbsorptionViewController: LoopChartsTableViewController, Identif
512512
carbEntryViewController.preferredCarbUnit = deviceManager.carbStore.preferredUnit
513513
navigationWrapper = UINavigationController(rootViewController: carbEntryViewController)
514514
} else {
515-
let viewModel = SimpleBolusViewModel(delegate: deviceManager)
516-
let bolusEntryView = SimpleBolusView(displayMealEntry: true, viewModel: viewModel)
515+
let viewModel = SimpleBolusViewModel(delegate: deviceManager, displayMealEntry: true)
516+
let bolusEntryView = SimpleBolusView(viewModel: viewModel)
517517
let hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false)
518518
navigationWrapper = UINavigationController(rootViewController: hostingController)
519519
hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation))

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,11 +1212,11 @@ final class StatusTableViewController: LoopChartsTableViewController {
12121212
}
12131213
navigationWrapper = UINavigationController(rootViewController: carbEntryViewController)
12141214
} else {
1215-
let viewModel = SimpleBolusViewModel(delegate: deviceManager)
1215+
let viewModel = SimpleBolusViewModel(delegate: deviceManager, displayMealEntry: true)
12161216
if let activity = activity {
12171217
viewModel.restoreUserActivityState(activity)
12181218
}
1219-
let bolusEntryView = SimpleBolusView(displayMealEntry: true, viewModel: viewModel).environmentObject(deviceManager.displayGlucoseUnitObservable)
1219+
let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(deviceManager.displayGlucoseUnitObservable)
12201220
let hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false)
12211221
navigationWrapper = UINavigationController(rootViewController: hostingController)
12221222
hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: navigationWrapper, action: #selector(dismissWithAnimation))
@@ -1235,8 +1235,8 @@ final class StatusTableViewController: LoopChartsTableViewController {
12351235
let bolusEntryView = BolusEntryView(viewModel: viewModel).environmentObject(deviceManager.displayGlucoseUnitObservable)
12361236
hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false)
12371237
} else {
1238-
let viewModel = SimpleBolusViewModel(delegate: deviceManager)
1239-
let bolusEntryView = SimpleBolusView(displayMealEntry: false, viewModel: viewModel).environmentObject(deviceManager.displayGlucoseUnitObservable)
1238+
let viewModel = SimpleBolusViewModel(delegate: deviceManager, displayMealEntry: false)
1239+
let bolusEntryView = SimpleBolusView(viewModel: viewModel).environmentObject(deviceManager.displayGlucoseUnitObservable)
12401240
hostingController = DismissibleHostingController(rootView: bolusEntryView, isModalInPresentation: false)
12411241
}
12421242
let navigationWrapper = UINavigationController(rootViewController: hostingController)

0 commit comments

Comments
 (0)