diff --git a/Cartfile.resolved b/Cartfile.resolved index 7ac7fc8d67..85ee8dec1a 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,7 @@ github "LoopKit/Amplitude-iOS" "2137d5fd44bf630ed33e1e72d7af6d8f8612f270" github "LoopKit/CGMBLEKit" "1f9d0f221b886f12bcb2c46dc0b551589b623210" -github "LoopKit/LoopKit" "7082c64333113ad83238e483e83029c161ac35bb" +github "LoopKit/LoopKit" "16fc21d01a7bf0758c6c81dbb25a5168588c9e30" github "i-schuetz/SwiftCharts" "0.6.1" github "mddub/G4ShareSpy" "v0.3.3" github "mddub/dexcom-share-client-swift" "v0.4.1" -github "ps2/rileylink_ios" "4a714a59d399aeb2984cbbe0434f02e6b59cfec5" +github "ps2/rileylink_ios" "143da3be985a98e602b15761f0cb0a092eaa9300" diff --git a/Common/Extensions/OSLog.swift b/Common/Extensions/OSLog.swift index 995281eb88..8fa95b7525 100644 --- a/Common/Extensions/OSLog.swift +++ b/Common/Extensions/OSLog.swift @@ -21,6 +21,10 @@ extension OSLog { log(message, type: .info, args) } + func `default`(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .default, args) + } + func error(_ message: StaticString, _ args: CVarArg...) { log(message, type: .error, args) } @@ -35,6 +39,10 @@ extension OSLog { os_log(message, log: self, type: type, args[0], args[1]) case 3: os_log(message, log: self, type: type, args[0], args[1], args[2]) + case 4: + os_log(message, log: self, type: type, args[0], args[1], args[2], args[3]) + case 5: + os_log(message, log: self, type: type, args[0], args[1], args[2], args[3], args[4]) default: os_log(message, log: self, type: type, args) } diff --git a/Loop/Managers/CGM/DexCGMManager.swift b/Loop/Managers/CGM/DexCGMManager.swift index 94a08b4abc..1ea71d3ad7 100644 --- a/Loop/Managers/CGM/DexCGMManager.swift +++ b/Loop/Managers/CGM/DexCGMManager.swift @@ -372,6 +372,13 @@ enum CalibrationError: Error { extension CalibrationError: LocalizedError { var errorDescription: String? { + switch self { + case .unreliableState: + return NSLocalizedString("Glucose data is unavailable", comment: "Error description for unreliable state") + } + } + + var failureReason: String? { switch self { case .unreliableState(let state): return state.localizedDescription diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 5abaecb740..b73be05a22 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -170,6 +170,8 @@ extension DeviceDataManager: PumpManagerDelegate { if let newValue = pumpManager.pumpBatteryChargeRemaining { if newValue == 0 { NotificationManager.sendPumpBatteryLowNotification() + } else { + NotificationManager.clearPumpBatteryLowNotification() } if let oldValue = oldValue, newValue - oldValue >= 0.5 { @@ -246,14 +248,20 @@ extension DeviceDataManager: PumpManagerDelegate { return } + var didSendLowNotification = false let warningThresholds: [Double] = [10, 20, 30] for threshold in warningThresholds { if newValue.unitVolume <= threshold && previousVolume > threshold { NotificationManager.sendPumpReservoirLowNotificationForAmount(newValue.unitVolume, andTimeRemaining: nil) + didSendLowNotification = true } } + if !didSendLowNotification { + NotificationManager.clearPumpReservoirNotification() + } + if newValue.unitVolume > previousVolume + 1 { AnalyticsManager.shared.reservoirWasRewound() } diff --git a/Loop/Managers/DiagnosticLogger.swift b/Loop/Managers/DiagnosticLogger.swift index cac510f461..a02a85b5cd 100644 --- a/Loop/Managers/DiagnosticLogger.swift +++ b/Loop/Managers/DiagnosticLogger.swift @@ -8,6 +8,7 @@ import Foundation import os.log +import LoopKit final class DiagnosticLogger { @@ -106,6 +107,11 @@ final class CategoryLogger { remoteLog(.info, message: message) } + func `default`(_ message: String) { + systemLog.info("%{public}@", message) + remoteLog(.default, message: message) + } + func error(_ message: [String: Any]) { systemLog.error("%{public}@", String(reflecting: message)) remoteLog(.error, message: message) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 555854f0a0..2d5549d299 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -189,6 +189,7 @@ final class LoopDataManager { set { lockedLastLoopCompleted.value = newValue + NotificationManager.clearLoopNotRunningNotifications() NotificationManager.scheduleLoopNotRunningNotifications() AnalyticsManager.shared.loopDidSucceed() } diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift index ac4daa78c1..3f23912e6b 100644 --- a/Loop/Managers/NotificationManager.swift +++ b/Loop/Managers/NotificationManager.swift @@ -63,12 +63,25 @@ struct NotificationManager { notification.title = NSLocalizedString("Bolus", comment: "The notification title for a bolus failure") + let sentenceFormat = NSLocalizedString("%@.", comment: "Appends a full-stop to a statement") + switch error { case let error as SetBolusError: notification.subtitle = error.errorDescriptionWithUnits(units) - notification.body = String(format: "%@ %@", error.failureReason!, error.recoverySuggestion!) + + let body = [error.failureReason, error.recoverySuggestion].compactMap({ $0 }).map({ + String(format: sentenceFormat, $0) + }).joined(separator: " ") + + notification.body = body case let error as LocalizedError: - notification.body = error.errorDescription ?? error.localizedDescription + if let subtitle = error.errorDescription { + notification.subtitle = subtitle + } + let message = [error.failureReason, error.recoverySuggestion].compactMap({ $0 }).map({ + String(format: sentenceFormat, $0) + }).joined(separator: "\n") + notification.body = message.isEmpty ? String(describing: error) : message default: notification.body = error.localizedDescription } @@ -134,6 +147,19 @@ struct NotificationManager { } } + static func clearLoopNotRunningNotifications() { + // Clear out any existing not-running notifications + UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in + let loopNotRunningIdentifiers = notifications.filter({ + $0.request.content.categoryIdentifier == Category.loopNotRunning.rawValue + }).map({ + $0.request.identifier + }) + + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: loopNotRunningIdentifiers) + } + } + static func sendPumpBatteryLowNotification() { let notification = UNMutableNotificationContent() @@ -151,6 +177,10 @@ struct NotificationManager { UNUserNotificationCenter.current().add(request) } + static func clearPumpBatteryLowNotification() { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [Category.pumpBatteryLow.rawValue]) + } + static func sendPumpReservoirEmptyNotification() { let notification = UNMutableNotificationContent() @@ -200,4 +230,8 @@ struct NotificationManager { UNUserNotificationCenter.current().add(request) } + + static func clearPumpReservoirNotification() { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [Category.pumpReservoirLow.rawValue]) + } } diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index 7fa84e56fb..0ac032527f 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -28,9 +28,11 @@ final class SettingsTableViewController: UITableViewController { } override func viewWillAppear(_ animated: Bool) { - // Manually invoke the delegate for rows deselecting on appear - for indexPath in tableView.indexPathsForSelectedRows ?? [] { - _ = tableView(tableView, willDeselectRowAt: indexPath) + if clearsSelectionOnViewWillAppear { + // Manually invoke the delegate for rows deselecting on appear + for indexPath in tableView.indexPathsForSelectedRows ?? [] { + _ = tableView(tableView, willDeselectRowAt: indexPath) + } } super.viewWillAppear(animated)