Skip to content

Commit af767ae

Browse files
authored
LOOP-1057: Support for in-app and user notification alerts (#64)
* checkpoint * checkpoint - kind of working * checkpoint * checkpoint * tweak * unused import * Fix a bug introduced when I cleaned up the code * PR Feedback * PR Feedback * Wrap up the `managerIdentifier` and `typeIdentifier` into a single type, `Identifier` * PR Feedback: renaming: UserAlert* -> DeviceAlert* scheduleAlert -> issueAlert unscheduleAlert -> removePendingAlerts cancelAlert -> removeDeliveredAlerts * More renaming, move acknowledgeDeviceAlert to DeviceAlertManager (where it really should've been) * Project file, removeAlertResponder * More PR Feedback: get rid of AcknowledgeCompletion * added a comment * PR feedback: renaming only deviceManagerInstanceIdentifier -> deviceManagerIdentifier removePendingAlerts -> removePendingAlert removeDeliveredAlerts -> removeDeliveredAlert * More PR Feedback: - Made `alertsShowing` and `alertsPending` a dictionary, by request. Also took the opportunity to add some bulletproofing "preconditions". - Also, a bonus: I didn't like the asymmetry of the `DeviceAlertManagerResponder` protocol, and it was unnecessary. So a small change there. * PR feedback: one more rename: deviceManagerIdentifier -> managerIdentifier
1 parent 9342700 commit af767ae

File tree

7 files changed

+381
-39
lines changed

7 files changed

+381
-39
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
/* End PBXAggregateTarget section */
2323

2424
/* Begin PBXBuildFile section */
25+
1DA649A7244126CD00F61E75 /* UserNotificationDeviceAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A6244126CD00F61E75 /* UserNotificationDeviceAlertHandler.swift */; };
26+
1DA649A9244126DA00F61E75 /* InAppModalDeviceAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A8244126DA00F61E75 /* InAppModalDeviceAlertHandler.swift */; };
27+
1DB1065124467E18005542BD /* DeviceAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB1065024467E18005542BD /* DeviceAlertManager.swift */; };
2528
43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; };
2629
4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; };
2730
4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; };
@@ -561,6 +564,9 @@
561564
/* End PBXCopyFilesBuildPhase section */
562565

563566
/* Begin PBXFileReference section */
567+
1DA649A6244126CD00F61E75 /* UserNotificationDeviceAlertHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationDeviceAlertHandler.swift; sourceTree = "<group>"; };
568+
1DA649A8244126DA00F61E75 /* InAppModalDeviceAlertHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppModalDeviceAlertHandler.swift; sourceTree = "<group>"; };
569+
1DB1065024467E18005542BD /* DeviceAlertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceAlertManager.swift; sourceTree = "<group>"; };
564570
4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = "<group>"; };
565571
4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = "<group>"; };
566572
430B29892041F54A00BA9F93 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = "<group>"; };
@@ -1145,6 +1151,16 @@
11451151
/* End PBXFrameworksBuildPhase section */
11461152

11471153
/* Begin PBXGroup section */
1154+
1DA6499D2441266400F61E75 /* DeviceAlert */ = {
1155+
isa = PBXGroup;
1156+
children = (
1157+
1DB1065024467E18005542BD /* DeviceAlertManager.swift */,
1158+
1DA649A8244126DA00F61E75 /* InAppModalDeviceAlertHandler.swift */,
1159+
1DA649A6244126CD00F61E75 /* UserNotificationDeviceAlertHandler.swift */,
1160+
);
1161+
path = DeviceAlert;
1162+
sourceTree = "<group>";
1163+
};
11481164
4328E0121CFBE1B700E199AA /* Controllers */ = {
11491165
isa = PBXGroup;
11501166
children = (
@@ -1541,6 +1557,7 @@
15411557
43F5C2E41B93C5D4003EB13D /* Managers */ = {
15421558
isa = PBXGroup;
15431559
children = (
1560+
1DA6499D2441266400F61E75 /* DeviceAlert */,
15441561
439897361CD2F80600223065 /* AnalyticsServicesManager.swift */,
15451562
439BED291E76093C00B0AED5 /* CGMManager.swift */,
15461563
43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */,
@@ -2515,11 +2532,13 @@
25152532
C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */,
25162533
89D6953E23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift in Sources */,
25172534
89CA2B30226C0161004D9350 /* DirectoryObserver.swift in Sources */,
2535+
1DA649A7244126CD00F61E75 /* UserNotificationDeviceAlertHandler.swift in Sources */,
25182536
439A7942211F631C0041B75F /* RootNavigationController.swift in Sources */,
25192537
4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */,
25202538
43F5C2DB1B92A5E1003EB13D /* SettingsTableViewController.swift in Sources */,
25212539
89E267FC2292456700A3F2AF /* FeatureFlags.swift in Sources */,
25222540
43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */,
2541+
1DA649A9244126DA00F61E75 /* InAppModalDeviceAlertHandler.swift in Sources */,
25232542
43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */,
25242543
4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */,
25252544
43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */,
@@ -2538,6 +2557,7 @@
25382557
89CA2B32226C18B8004D9350 /* TestingScenariosTableViewController.swift in Sources */,
25392558
43E93FB71E469A5100EAB8DB /* HKUnit.swift in Sources */,
25402559
43C05CAF21EB2C24006FB252 /* NSBundle.swift in Sources */,
2560+
1DB1065124467E18005542BD /* DeviceAlertManager.swift in Sources */,
25412561
43BFF0BC1E45C80600FF19A9 /* UIColor+Loop.swift in Sources */,
25422562
43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */,
25432563
43D9003321EB258C00AF44BF /* InsulinModelSettings+Loop.swift in Sources */,

Loop/AppDelegate.swift

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,36 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
1919

2020
private lazy var pluginManager = PluginManager()
2121

22-
private lazy var deviceManager = DeviceDataManager(pluginManager: pluginManager)
23-
22+
private var deviceDataManager: DeviceDataManager!
23+
private var deviceAlertManager: DeviceAlertManager!
24+
2425
var window: UIWindow?
2526

2627
private var rootViewController: RootNavigationController! {
2728
return window?.rootViewController as? RootNavigationController
2829
}
2930

3031
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
31-
SharedLogging.instance = deviceManager.loggingServicesManager
32+
deviceAlertManager = DeviceAlertManager(rootViewController: rootViewController,
33+
isAppInBackgroundFunc: isInBackground)
34+
deviceDataManager = DeviceDataManager(pluginManager: pluginManager, deviceAlertManager: deviceAlertManager)
35+
36+
SharedLogging.instance = deviceDataManager.loggingServicesManager
3237

3338
NotificationManager.authorize(delegate: self)
3439

3540
log.info(#function)
3641

37-
deviceManager.analyticsServicesManager.application(application, didFinishLaunchingWithOptions: launchOptions)
42+
deviceDataManager.analyticsServicesManager.application(application, didFinishLaunchingWithOptions: launchOptions)
3843

39-
rootViewController.rootViewController.deviceManager = deviceManager
44+
rootViewController.rootViewController.deviceManager = deviceDataManager
4045

4146
let notificationOption = launchOptions?[.remoteNotification]
4247

4348
if let notification = notificationOption as? [String: AnyObject] {
44-
deviceManager.handleRemoteNotification(notification)
49+
deviceDataManager.handleRemoteNotification(notification)
4550
}
46-
51+
4752
return true
4853
}
4954

@@ -57,7 +62,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
5762
}
5863

5964
func applicationDidBecomeActive(_ application: UIApplication) {
60-
deviceManager.updatePumpManagerBLEHeartbeatPreference()
65+
deviceDataManager.updatePumpManagerBLEHeartbeatPreference()
6166
}
6267

6368
func applicationWillTerminate(_ application: UIApplication) {
@@ -91,7 +96,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
9196
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
9297
let token = tokenParts.joined()
9398
log.default("RemoteNotifications device token: %{public}@", token)
94-
deviceManager.loopManager.settings.deviceToken = deviceToken
99+
deviceDataManager.loopManager.settings.deviceToken = deviceToken
95100
}
96101

97102
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
@@ -107,10 +112,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
107112
return
108113
}
109114

110-
deviceManager.handleRemoteNotification(notification)
115+
deviceDataManager.handleRemoteNotification(notification)
111116
completionHandler(.noData)
112117
}
113118

119+
private func isInBackground() -> Bool {
120+
return UIApplication.shared.applicationState == .background
121+
}
114122
}
115123

116124

@@ -122,16 +130,19 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
122130
let startDate = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusStartDate.rawValue] as? Date,
123131
startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5)
124132
{
125-
deviceManager.analyticsServicesManager.didRetryBolus()
133+
deviceDataManager.analyticsServicesManager.didRetryBolus()
126134

127-
deviceManager.enactBolus(units: units, at: startDate) { (_) in
135+
deviceDataManager.enactBolus(units: units, at: startDate) { (_) in
128136
completionHandler()
129137
}
130138
return
131139
}
132-
case NotificationManager.Action.acknowledgeCGMAlert.rawValue:
133-
if let alertID = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.cgmAlertID.rawValue] as? Int {
134-
deviceManager.acknowledgeCGMAlert(alertID: alertID)
140+
case NotificationManager.Action.acknowledgeAlert.rawValue:
141+
let userInfo = response.notification.request.content.userInfo
142+
if let alertTypeIdentifier = userInfo[LoopNotificationUserInfoKey.alertTypeId.rawValue] as? DeviceAlert.TypeIdentifier,
143+
let managerIdentifier = userInfo[LoopNotificationUserInfoKey.managerIDForAlert.rawValue] as? String {
144+
deviceAlertManager.acknowledgeDeviceAlert(identifier:
145+
DeviceAlert.Identifier(managerIdentifier: managerIdentifier, typeIdentifier: alertTypeIdentifier))
135146
}
136147
default:
137148
break
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// DeviceAlertManager.swift
3+
// Loop
4+
//
5+
// Created by Rick Pasetto on 4/9/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import LoopKit
10+
11+
protocol DeviceAlertManagerResponder: class {
12+
/// Method for our Handlers to call to kick off alert response. Differs from DeviceAlertResponder because here we need the whole `Identifier`.
13+
func acknowledgeDeviceAlert(identifier: DeviceAlert.Identifier)
14+
}
15+
16+
/// Main (singleton-ish) class that is responsible for:
17+
/// - managing the different targets (handlers) that will post alerts
18+
/// - managing the different responders that might acknowledge the alert
19+
/// - serializing alerts to storage
20+
/// - etc.
21+
public final class DeviceAlertManager {
22+
23+
var handlers: [DeviceAlertHandler] = []
24+
var responders: [String: Weak<DeviceAlertResponder>] = [:]
25+
26+
public init(rootViewController: UIViewController,
27+
isAppInBackgroundFunc: @escaping () -> Bool) {
28+
handlers = [UserNotificationDeviceAlertHandler(isAppInBackgroundFunc: isAppInBackgroundFunc),
29+
InAppModalDeviceAlertHandler(rootViewController: rootViewController, deviceAlertManagerResponder: self)]
30+
}
31+
32+
public func addAlertResponder(key: String, alertResponder: DeviceAlertResponder) {
33+
responders[key] = Weak(alertResponder)
34+
}
35+
36+
public func removeAlertResponder(key: String) {
37+
responders.removeValue(forKey: key)
38+
}
39+
}
40+
41+
extension DeviceAlertManager: DeviceAlertManagerResponder {
42+
func acknowledgeDeviceAlert(identifier: DeviceAlert.Identifier) {
43+
if let responder = responders[identifier.managerIdentifier]?.value {
44+
responder.acknowledgeAlert(typeIdentifier: identifier.typeIdentifier)
45+
}
46+
}
47+
}
48+
49+
extension DeviceAlertManager: DeviceAlertHandler {
50+
51+
public func issueAlert(_ alert: DeviceAlert) {
52+
handlers.forEach { $0.issueAlert(alert) }
53+
}
54+
public func removePendingAlert(identifier: DeviceAlert.Identifier) {
55+
handlers.forEach { $0.removePendingAlert(identifier: identifier) }
56+
}
57+
public func removeDeliveredAlert(identifier: DeviceAlert.Identifier) {
58+
handlers.forEach { $0.removeDeliveredAlert(identifier: identifier) }
59+
}
60+
}
61+
62+
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//
2+
// InAppUserAlertHandler.swift
3+
// LoopKit
4+
//
5+
// Created by Rick Pasetto on 4/9/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import LoopKit
11+
12+
public class InAppModalDeviceAlertHandler: DeviceAlertHandler {
13+
14+
private weak var rootViewController: UIViewController?
15+
private weak var deviceAlertManagerResponder: DeviceAlertManagerResponder?
16+
17+
private var alertsShowing: [DeviceAlert.Identifier: (UIAlertController, DeviceAlert)] = [:]
18+
private var alertsPending: [DeviceAlert.Identifier: (Timer, DeviceAlert)] = [:]
19+
20+
init(rootViewController: UIViewController, deviceAlertManagerResponder: DeviceAlertManagerResponder) {
21+
self.rootViewController = rootViewController
22+
self.deviceAlertManagerResponder = deviceAlertManagerResponder
23+
}
24+
25+
public func issueAlert(_ alert: DeviceAlert) {
26+
switch alert.trigger {
27+
case .immediate:
28+
show(alert: alert)
29+
case .delayed(let interval):
30+
schedule(alert: alert, interval: interval, repeats: false)
31+
case .repeating(let interval):
32+
schedule(alert: alert, interval: interval, repeats: true)
33+
}
34+
}
35+
36+
public func removePendingAlert(identifier: DeviceAlert.Identifier) {
37+
DispatchQueue.main.async {
38+
self.alertsPending[identifier]?.0.invalidate()
39+
self.clearPendingAlert(identifier: identifier)
40+
}
41+
}
42+
43+
public func removeDeliveredAlert(identifier: DeviceAlert.Identifier) {
44+
DispatchQueue.main.async {
45+
self.alertsShowing[identifier]?.0.dismiss(animated: true)
46+
self.clearDeliveredAlert(identifier: identifier)
47+
}
48+
}
49+
}
50+
51+
/// Private functions
52+
extension InAppModalDeviceAlertHandler {
53+
54+
private func schedule(alert: DeviceAlert, interval: TimeInterval, repeats: Bool) {
55+
guard alert.foregroundContent != nil else {
56+
return
57+
}
58+
DispatchQueue.main.async {
59+
if self.isAlertPending(identifier: alert.identifier) {
60+
return
61+
}
62+
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats) { [weak self] timer in
63+
self?.show(alert: alert)
64+
if !repeats {
65+
self?.clearPendingAlert(identifier: alert.identifier)
66+
}
67+
}
68+
self.addPendingAlert(alert: alert, timer: timer)
69+
}
70+
}
71+
72+
private func show(alert: DeviceAlert) {
73+
guard let content = alert.foregroundContent else {
74+
return
75+
}
76+
DispatchQueue.main.async {
77+
if self.isAlertShowing(identifier: alert.identifier) {
78+
return
79+
}
80+
let alertController = self.presentAlert(title: content.title, message: content.body, action: content.acknowledgeActionButtonLabel) { [weak self] in
81+
self?.clearDeliveredAlert(identifier: alert.identifier)
82+
self?.deviceAlertManagerResponder?.acknowledgeDeviceAlert(identifier: alert.identifier)
83+
}
84+
self.addDeliveredAlert(alert: alert, controller: alertController)
85+
}
86+
}
87+
88+
private func addPendingAlert(alert: DeviceAlert, timer: Timer) {
89+
dispatchPrecondition(condition: .onQueue(.main))
90+
self.alertsPending[alert.identifier] = (timer, alert)
91+
}
92+
93+
private func addDeliveredAlert(alert: DeviceAlert, controller: UIAlertController) {
94+
dispatchPrecondition(condition: .onQueue(.main))
95+
self.alertsShowing[alert.identifier] = (controller, alert)
96+
}
97+
98+
private func clearPendingAlert(identifier: DeviceAlert.Identifier) {
99+
dispatchPrecondition(condition: .onQueue(.main))
100+
alertsPending[identifier] = nil
101+
}
102+
103+
private func clearDeliveredAlert(identifier: DeviceAlert.Identifier) {
104+
dispatchPrecondition(condition: .onQueue(.main))
105+
alertsShowing[identifier] = nil
106+
}
107+
108+
private func isAlertPending(identifier: DeviceAlert.Identifier) -> Bool {
109+
dispatchPrecondition(condition: .onQueue(.main))
110+
return alertsPending.index(forKey: identifier) != nil
111+
}
112+
113+
private func isAlertShowing(identifier: DeviceAlert.Identifier) -> Bool {
114+
dispatchPrecondition(condition: .onQueue(.main))
115+
return alertsShowing.index(forKey: identifier) != nil
116+
}
117+
118+
private func presentAlert(title: String, message: String, action: String, completion: @escaping () -> Void) -> UIAlertController {
119+
dispatchPrecondition(condition: .onQueue(.main))
120+
// For now, this is a simple alert with an "OK" button
121+
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
122+
alertController.addAction(UIAlertAction(title: action, style: .default, handler: { _ in completion() }))
123+
topViewController(controller: rootViewController)?.present(alertController, animated: true)
124+
return alertController
125+
}
126+
127+
// Helper function pulled from SO...may be outdated, especially in the SwiftUI world
128+
private func topViewController(controller: UIViewController?) -> UIViewController? {
129+
if let tabController = controller as? UITabBarController {
130+
return topViewController(controller: tabController.selectedViewController)
131+
}
132+
if let navController = controller as? UINavigationController {
133+
return topViewController(controller: navController.visibleViewController)
134+
}
135+
if let presented = controller?.presentedViewController {
136+
return topViewController(controller: presented)
137+
}
138+
return controller
139+
}
140+
141+
}

0 commit comments

Comments
 (0)