Skip to content

Commit 0d98b00

Browse files
author
Darin Krauss
authored
[LOOP-2546] Display onboarding at startup (#372)
- https://tidepool.atlassian.net/browse/LOOP-2546 - https://tidepool.atlassian.net/browse/LOOP-3143 - https://tidepool.atlassian.net/browse/LOOP-3144 - https://tidepool.atlassian.net/browse/LOOP-3145 - Refactor AppDelegate into new LoopAppManager class - Refactor onboarding into new OnboardingManager class - Refactor BluetoothStateManager to support deferred authorization and separate state - Update root view controller dependent classes to use provider - Update LaunchScreen and Main root view controller to display system background only - Refactor and remove onboarding related from StatusTableViewController - Defer StatusTableViewController creation until after onboarding - Support multiple onboarding plugins - Use OSLog in PluginManager for debugging
1 parent a3cc9dd commit 0d98b00

26 files changed

+1277
-749
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 33 additions & 23 deletions
Large diffs are not rendered by default.

Loop/AppDelegate.swift

Lines changed: 35 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -6,111 +6,32 @@
66
// Copyright © 2015 Nathan Racklyeft. All rights reserved.
77
//
88

9-
import HealthKit
10-
import Intents
11-
import LoopCore
12-
import LoopKit
13-
import LoopKitUI
149
import UIKit
15-
import UserNotifications
10+
import LoopKit
1611

1712
@UIApplicationMain
18-
final class AppDelegate: UIResponder, UIApplicationDelegate, DeviceOrientationController {
19-
20-
private lazy var log = DiagnosticLog(category: "AppDelegate")
21-
22-
private lazy var pluginManager = PluginManager()
23-
24-
private var alertManager: AlertManager!
25-
private var deviceDataManager: DeviceDataManager!
26-
private var loopAlertsManager: LoopAlertsManager!
27-
private var bluetoothStateManager: BluetoothStateManager!
28-
private var trustedTimeChecker: TrustedTimeChecker!
29-
13+
final class AppDelegate: UIResponder, UIApplicationDelegate {
3014
var window: UIWindow?
31-
32-
var launchOptions: [UIApplication.LaunchOptionsKey: Any]?
33-
34-
private var rootViewController: RootNavigationController! {
35-
return window?.rootViewController as? RootNavigationController
36-
}
37-
38-
private func isAfterFirstUnlock() -> Bool {
39-
let fileManager = FileManager.default
40-
do {
41-
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
42-
let fileURL = documentDirectory.appendingPathComponent("protection.test")
43-
guard fileManager.fileExists(atPath: fileURL.path) else {
44-
let contents = Data("unimportant".utf8)
45-
try? contents.write(to: fileURL, options: .completeFileProtectionUntilFirstUserAuthentication)
46-
// If file doesn't exist, we're at first start, which will be user directed.
47-
return true
48-
}
49-
let contents = try? Data(contentsOf: fileURL)
50-
return contents != nil
51-
} catch {
52-
log.error("Could not create after first unlock test file: %@", String(describing: error))
53-
}
54-
return false
55-
}
56-
57-
private func finishLaunch(application: UIApplication) {
58-
log.default("Finishing launching")
59-
UIDevice.current.isBatteryMonitoringEnabled = true
60-
61-
bluetoothStateManager = BluetoothStateManager()
62-
alertManager = AlertManager(rootViewController: rootViewController, expireAfter: Bundle.main.localCacheDuration)
63-
deviceDataManager = DeviceDataManager(pluginManager: pluginManager, alertManager: alertManager, bluetoothStateManager: bluetoothStateManager, rootViewController: rootViewController)
64-
65-
let statusTableViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "MainStatusViewController") as! StatusTableViewController
66-
67-
statusTableViewController.deviceManager = deviceDataManager
68-
69-
bluetoothStateManager.addBluetoothStateObserver(statusTableViewController)
70-
71-
loopAlertsManager = LoopAlertsManager(alertManager: alertManager, bluetoothStateManager: bluetoothStateManager)
72-
73-
SharedLogging.instance = deviceDataManager.loggingServicesManager
74-
75-
deviceDataManager?.analyticsServicesManager.application(application, didFinishLaunchingWithOptions: launchOptions)
7615

77-
OrientationLock.deviceOrientationController = self
16+
private let loopAppManager = LoopAppManager()
17+
private let log = DiagnosticLog(category: "AppDelegate")
7818

79-
NotificationManager.authorize(delegate: self)
80-
81-
rootViewController.pushViewController(statusTableViewController, animated: false)
82-
83-
let notificationOption = launchOptions?[.remoteNotification]
84-
85-
if let notification = notificationOption as? [String: AnyObject] {
86-
deviceDataManager?.handleRemoteNotification(notification)
87-
}
88-
89-
scheduleBackgroundTasks()
90-
91-
launchOptions = nil
92-
93-
trustedTimeChecker = TrustedTimeChecker(alertManager)
94-
}
19+
// MARK: - UIApplicationDelegate - Initialization
9520

9621
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
97-
98-
self.launchOptions = launchOptions
99-
100-
log.default("didFinishLaunchingWithOptions %{public}@", String(describing: launchOptions))
101-
102-
registerBackgroundTasks()
22+
log.default("%{public}@ with launchOptions: %{public}@", #function, String(describing: launchOptions))
10323

104-
guard isAfterFirstUnlock() else {
105-
log.default("Launching before first unlock; pausing launch...")
106-
return false
107-
}
24+
loopAppManager.initialize(with: launchOptions)
25+
loopAppManager.launch(into: window)
26+
return loopAppManager.isLaunchComplete
27+
}
10828

109-
finishLaunch(application: application)
29+
// MARK: - UIApplicationDelegate - Life Cycle
11030

111-
window?.tintColor = .loopAccent
31+
func applicationDidBecomeActive(_ application: UIApplication) {
32+
log.default(#function)
11233

113-
return true
34+
loopAppManager.didBecomeActive()
11435
}
11536

11637
func applicationWillResignActive(_ application: UIApplication) {
@@ -125,139 +46,47 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, DeviceOrientationCo
12546
log.default(#function)
12647
}
12748

128-
func applicationDidBecomeActive(_ application: UIApplication) {
129-
deviceDataManager?.updatePumpManagerBLEHeartbeatPreference()
130-
}
131-
13249
func applicationWillTerminate(_ application: UIApplication) {
13350
log.default(#function)
13451
}
13552

136-
// MARK: - Continuity
137-
138-
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
139-
log.default(#function)
140-
141-
if #available(iOS 12.0, *) {
142-
if userActivity.activityType == NewCarbEntryIntent.className {
143-
log.default("Restoring %{public}@ intent", userActivity.activityType)
144-
rootViewController.restoreUserActivityState(.forNewCarbEntry())
145-
return true
146-
}
147-
}
148-
149-
switch userActivity.activityType {
150-
case NSUserActivity.newCarbEntryActivityType,
151-
NSUserActivity.viewLoopStatusActivityType:
152-
log.default("Restoring %{public}@ activity", userActivity.activityType)
153-
restorationHandler([rootViewController])
154-
return true
155-
default:
156-
return false
157-
}
158-
}
159-
160-
// MARK: - Remote notifications
161-
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
162-
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
163-
let token = tokenParts.joined()
164-
log.default("RemoteNotifications device token: %{public}@", token)
165-
deviceDataManager?.loopManager.settings.deviceToken = deviceToken
166-
}
53+
// MARK: - UIApplicationDelegate - Environment
16754

168-
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
169-
log.error("Failed to register: %{public}@", String(describing: error))
170-
}
171-
172-
func application(_ application: UIApplication,
173-
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
174-
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
175-
guard let notification = userInfo as? [String: AnyObject] else {
176-
completionHandler(.failed)
177-
return
178-
}
179-
180-
deviceDataManager?.handleRemoteNotification(notification)
181-
completionHandler(.noData)
182-
}
183-
18455
func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {
185-
log.default("applicationProtectedDataDidBecomeAvailable")
186-
187-
if deviceDataManager == nil {
188-
finishLaunch(application: application)
56+
if !loopAppManager.isLaunchComplete {
57+
loopAppManager.launch(into: window)
18958
}
19059
}
191-
192-
// MARK: - DeviceOrientationController
19360

194-
var supportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown
61+
// MARK: - UIApplicationDelegate - Remote Notification
19562

196-
func setOriginallySupportedInferfaceOrientations() {
197-
supportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown
198-
}
63+
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
64+
log.default(#function)
19965

200-
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
201-
supportedInterfaceOrientations
66+
loopAppManager.setRemoteNotificationsDeviceToken(deviceToken)
20267
}
20368

204-
// MARK: - Background Tasks
205-
206-
private func registerBackgroundTasks() {
207-
if DeviceDataManager.registerCriticalEventLogHistoricalExportBackgroundTask({ self.deviceDataManager.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) {
208-
log.debug("Critical event log export background task registered")
209-
} else {
210-
log.error("Critical event log export background task not registered")
211-
}
69+
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
70+
log.error("%{public}@ with error: %{public}@", #function, String(describing: error))
21271
}
21372

214-
private func scheduleBackgroundTasks() {
215-
deviceDataManager.scheduleCriticalEventLogHistoricalExportBackgroundTask()
216-
}
217-
}
73+
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
74+
log.default(#function)
21875

219-
// MARK: UNUserNotificationCenterDelegate implementation
76+
completionHandler(loopAppManager.handleRemoteNotification(userInfo as? [String: AnyObject]) ? .noData : .failed)
77+
}
22078

221-
extension AppDelegate: UNUserNotificationCenterDelegate {
222-
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
223-
switch response.actionIdentifier {
224-
case NotificationManager.Action.retryBolus.rawValue:
225-
if let units = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusAmount.rawValue] as? Double,
226-
let startDate = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusStartDate.rawValue] as? Date,
227-
startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5)
228-
{
229-
deviceDataManager?.analyticsServicesManager.didRetryBolus()
79+
// MARK: - UIApplicationDelegate - Continuity
23080

231-
deviceDataManager?.enactBolus(units: units, at: startDate) { (_) in
232-
completionHandler()
233-
}
234-
return
235-
}
236-
case NotificationManager.Action.acknowledgeAlert.rawValue:
237-
let userInfo = response.notification.request.content.userInfo
238-
if let alertIdentifier = userInfo[LoopNotificationUserInfoKey.alertTypeID.rawValue] as? Alert.AlertIdentifier,
239-
let managerIdentifier = userInfo[LoopNotificationUserInfoKey.managerIDForAlert.rawValue] as? String {
240-
alertManager.acknowledgeAlert(identifier:
241-
Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: alertIdentifier))
242-
}
243-
default:
244-
break
245-
}
81+
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
82+
log.default(#function)
24683

247-
completionHandler()
84+
return loopAppManager.userActivity(userActivity, restorationHandler: restorationHandler)
24885
}
24986

250-
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
251-
switch notification.request.identifier {
252-
// TODO: Until these notifications are converted to use the new alert system, they shall still show in the foreground
253-
case LoopNotificationCategory.bolusFailure.rawValue,
254-
LoopNotificationCategory.pumpBatteryLow.rawValue,
255-
LoopNotificationCategory.pumpExpired.rawValue,
256-
LoopNotificationCategory.pumpFault.rawValue:
257-
completionHandler([.badge, .sound, .alert])
258-
default:
259-
// All other userNotifications are not to be displayed while in the foreground
260-
completionHandler([])
261-
}
87+
// MARK: - UIApplicationDelegate - Interface
88+
89+
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
90+
return loopAppManager.supportedInterfaceOrientations
26291
}
26392
}

Loop/Base.lproj/LaunchScreen.storyboard

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
<!--Navigation Controller-->
1111
<scene sceneID="Ucb-uj-Fzl">
1212
<objects>
13-
<navigationController navigationBarHidden="YES" toolbarHidden="NO" id="c6k-8z-nla" sceneMemberID="viewController">
13+
<navigationController navigationBarHidden="YES" id="c6k-8z-nla" sceneMemberID="viewController">
1414
<tabBarItem key="tabBarItem" enabled="NO" title="" id="nfG-Bf-TrT"/>
15+
<nil key="simulatedTopBarMetrics"/>
16+
<nil key="simulatedBottomBarMetrics"/>
1517
<navigationBar key="navigationBar" contentMode="scaleToFill" id="3rC-hS-Fnr">
1618
<autoresizingMask key="autoresizingMask"/>
1719
</navigationBar>
1820
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="4Ow-gU-14I">
19-
<rect key="frame" x="0.0" y="813" width="414" height="49"/>
2021
<autoresizingMask key="autoresizingMask"/>
2122
</toolbar>
2223
<connections>

Loop/Base.lproj/Main.storyboard

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="OJt-dE-GaA">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="OJt-dE-GaA">
33
<device id="retina4_7" orientation="portrait" appearance="light"/>
44
<dependencies>
55
<deployment identifier="iOS"/>
6-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17126"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
77
<capability name="Named colors" minToolsVersion="9.0"/>
88
<capability name="System colors in document resources" minToolsVersion="11.0"/>
99
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -673,15 +673,18 @@
673673
<!--Root Navigation Controller-->
674674
<scene sceneID="II9-on-wj3">
675675
<objects>
676-
<navigationController automaticallyAdjustsScrollViewInsets="NO" toolbarHidden="NO" id="OJt-dE-GaA" customClass="RootNavigationController" customModule="Loop" customModuleProvider="target" sceneMemberID="viewController">
676+
<navigationController automaticallyAdjustsScrollViewInsets="NO" navigationBarHidden="YES" id="OJt-dE-GaA" customClass="RootNavigationController" customModule="Loop" customModuleProvider="target" sceneMemberID="viewController">
677+
<nil key="simulatedTopBarMetrics"/>
678+
<nil key="simulatedBottomBarMetrics"/>
677679
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="lcU-HN-Qiy">
678-
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
679680
<autoresizingMask key="autoresizingMask"/>
680681
</navigationBar>
681682
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="K8q-nd-eVx">
682-
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
683683
<autoresizingMask key="autoresizingMask"/>
684684
</toolbar>
685+
<connections>
686+
<segue destination="csl-Ke-OPG" kind="relationship" relationship="rootViewController" id="2Ti-Uv-cxR"/>
687+
</connections>
685688
</navigationController>
686689
<placeholder placeholderIdentifier="IBFirstResponder" id="Z8K-4u-wX1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
687690
</objects>
@@ -904,6 +907,25 @@
904907
</objects>
905908
<point key="canvasLocation" x="2458" y="733"/>
906909
</scene>
910+
<!--View Controller-->
911+
<scene sceneID="f78-Bb-hbV">
912+
<objects>
913+
<viewController id="csl-Ke-OPG" sceneMemberID="viewController">
914+
<layoutGuides>
915+
<viewControllerLayoutGuide type="top" id="gFu-6V-bzf"/>
916+
<viewControllerLayoutGuide type="bottom" id="iEe-G5-n7b"/>
917+
</layoutGuides>
918+
<view key="view" contentMode="scaleToFill" id="VRE-WG-94h">
919+
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
920+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
921+
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
922+
</view>
923+
<navigationItem key="navigationItem" id="HVC-1r-kuZ"/>
924+
</viewController>
925+
<placeholder placeholderIdentifier="IBFirstResponder" id="S8O-1K-8tW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
926+
</objects>
927+
<point key="canvasLocation" x="838" y="323"/>
928+
</scene>
907929
</scenes>
908930
<resources>
909931
<image name="Oval Selection" width="21" height="21"/>

0 commit comments

Comments
 (0)