diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index 9519d4a64d..d7f3763134 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -343,6 +343,16 @@ + @@ -350,10 +360,12 @@ + + @@ -362,6 +374,7 @@ + diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 7732b94ede..089138f43f 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -62,7 +62,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } var sensorInfo: SensorDisplayable? { - return latestGlucoseG5 ?? latestGlucoseG4 ?? latestPumpStatusFromMySentry + return latestGlucoseG5 ?? latestGlucoseG4 ?? latestGlucoseFromShare ?? latestPumpStatusFromMySentry } // MARK: - RileyLink @@ -492,6 +492,8 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter private var latestGlucoseG5: xDripG5.Glucose? + private var latestGlucoseFromShare: ShareGlucose? + /** Attempts to backfill glucose data from the share servers if a G5 connection hasn't been established. @@ -519,6 +521,8 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter return } + self.latestGlucoseFromShare = glucose.first + // Ignore glucose values that are up to a minute newer than our previous value, to account for possible time shifting in Share data let newGlucose = glucose.filterDateRange(glucoseStore.latestGlucose?.startDate.dateByAddingTimeInterval(NSTimeInterval(minutes: 1)), nil).map { return (quantity: $0.quantity, date: $0.startDate, isDisplayOnly: false) @@ -560,7 +564,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter let includeAfter = glucoseStore.latestGlucose?.startDate.dateByAddingTimeInterval(NSTimeInterval(minutes: 1)) let validGlucose = glucoseHistory.flatMap({ - $0.isValid ? $0 : nil + $0.isStateValid ? $0 : nil }).filterDateRange(includeAfter, nil).map({ (quantity: $0.quantity, date: $0.startDate, isDisplayOnly: $0.isDisplayOnly) }) diff --git a/Loop/Models/Glucose.swift b/Loop/Models/Glucose.swift index c0c2db1544..72aec6e4e8 100644 --- a/Loop/Models/Glucose.swift +++ b/Loop/Models/Glucose.swift @@ -11,6 +11,10 @@ import xDripG5 extension Glucose: SensorDisplayable { + var isStateValid: Bool { + return state == .OK && status == .OK + } + var stateDescription: String { let status: String switch self.status { diff --git a/Loop/Models/GlucoseG4.swift b/Loop/Models/GlucoseG4.swift index 3510a43eb7..e87623fc51 100644 --- a/Loop/Models/GlucoseG4.swift +++ b/Loop/Models/GlucoseG4.swift @@ -12,13 +12,6 @@ import HealthKit import LoopKit -extension GlucoseG4 { - var isValid: Bool { - return glucose >= 20 - } -} - - extension GlucoseG4: GlucoseValue { public var quantity: HKQuantity { return HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucose)) @@ -31,11 +24,15 @@ extension GlucoseG4: GlucoseValue { extension GlucoseG4: SensorDisplayable { + var isStateValid: Bool { + return glucose >= 20 + } + var stateDescription: String { - if isValid { - return "✓" + if isStateValid { + return NSLocalizedString("OK", comment: "Sensor state description for the valid state") } else { - return String(format: "%02x", glucose) + return NSLocalizedString("Needs Attention", comment: "Sensor state description for the non-valid state") } } diff --git a/Loop/Models/MySentryPumpStatusMessageBody.swift b/Loop/Models/MySentryPumpStatusMessageBody.swift index 631995cfba..f1535c6816 100644 --- a/Loop/Models/MySentryPumpStatusMessageBody.swift +++ b/Loop/Models/MySentryPumpStatusMessageBody.swift @@ -11,14 +11,12 @@ import MinimedKit extension MySentryPumpStatusMessageBody: SensorDisplayable { - var stateDescription: String { + var isStateValid: Bool { switch glucose { - case .Active: - return "✓" - case .Off: - return "" + case .Active, .Off: + return true default: - return String(glucose) + return false } } diff --git a/Loop/Models/SensorDisplayable.swift b/Loop/Models/SensorDisplayable.swift index bdcb516a51..7cf7723110 100644 --- a/Loop/Models/SensorDisplayable.swift +++ b/Loop/Models/SensorDisplayable.swift @@ -6,11 +6,27 @@ // Copyright © 2016 Nathan Racklyeft. All rights reserved. // +import Foundation + protocol SensorDisplayable { - // Describes the state of the sensor in the current localization + /// Returns whether the current state is valid + var isStateValid: Bool { get } + + /// Describes the state of the sensor in the current localization var stateDescription: String { get } /// Enumerates the trend of the sensor values var trendType: GlucoseTrend? { get } } + + +extension SensorDisplayable { + var stateDescription: String { + if isStateValid { + return NSLocalizedString("OK", comment: "Sensor state description for the valid state") + } else { + return NSLocalizedString("Needs Attention", comment: "Sensor state description for the non-valid state") + } + } +} diff --git a/Loop/Models/ShareGlucose+GlucoseKit.swift b/Loop/Models/ShareGlucose+GlucoseKit.swift index ef93b5694d..8699c1e9a6 100644 --- a/Loop/Models/ShareGlucose+GlucoseKit.swift +++ b/Loop/Models/ShareGlucose+GlucoseKit.swift @@ -21,3 +21,14 @@ extension ShareGlucose: GlucoseValue { return HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucose)) } } + + +extension ShareGlucose: SensorDisplayable { + var isStateValid: Bool { + return glucose >= 20 + } + + var trendType: GlucoseTrend? { + return GlucoseTrend(rawValue: Int(trend)) + } +} diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index 59c5c0836a..a6577dfc8d 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -8,6 +8,8 @@ import UIKit import HealthKit +import LoopKit + class PredictionTableViewController: UITableViewController, IdentifiableClass { @@ -85,12 +87,6 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass { var dataManager: DeviceDataManager! - private var active = true { - didSet { - reloadData() - } - } - private lazy var charts: StatusChartsManager = { let charts = StatusChartsManager() @@ -102,6 +98,14 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass { return charts }() + private var retrospectivePredictedGlucose: [GlucoseValue]? + + private var active = true { + didSet { + reloadData() + } + } + private var needsRefresh = true private var visible = false { @@ -149,11 +153,12 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass { } dispatch_group_enter(reloadGroup) - dataManager.loopManager.getLoopStatus { (predictedGlucose, _, _, _, _, _, error) in + dataManager.loopManager.getLoopStatus { (predictedGlucose, retrospectivePredictedGlucose, _, _, _, _, error) in if error != nil { self.needsRefresh = true } + self.retrospectivePredictedGlucose = retrospectivePredictedGlucose self.charts.predictedGlucoseValues = predictedGlucose ?? [] dispatch_group_leave(reloadGroup) @@ -180,7 +185,7 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass { self.charts.prerender() self.tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(Section.charts.rawValue, 1)), - withRowAnimation: animated ? .Fade : .None + withRowAnimation: .None ) self.reloading = false @@ -242,11 +247,29 @@ class PredictionTableViewController: UITableViewController, IdentifiableClass { let (input, selected) = selectedInputs[indexPath.row] cell.titleLabel?.text = input.localizedTitle - cell.subtitleLabel?.text = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) cell.accessoryType = selected ? .Checkmark : .None - cell.enabled = input != .retrospection || dataManager.loopManager.retrospectiveCorrectionEnabled + var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) + + if input == .retrospection, + let startGlucose = retrospectivePredictedGlucose?.first, + let endGlucose = retrospectivePredictedGlucose?.last, + let currentGlucose = self.dataManager.glucoseStore?.latestGlucose + { + let formatter = NSNumberFormatter.glucoseFormatter(for: charts.glucoseUnit) + let values = [startGlucose, endGlucose, currentGlucose].map { formatter.stringFromNumber($0.quantity.doubleValueForUnit(charts.glucoseUnit)) ?? "?" } + + let retro = String( + format: NSLocalizedString("Last comparison: %1$@ → %2$@ vs %3$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)"), + values[0], values[1], values[2] + ) + + subtitleText = String(format: "%@\n%@", subtitleText, retro) + } + + cell.subtitleLabel?.text = subtitleText + cell.contentView.layoutMargins.left = tableView.separatorInset.left return cell diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 8d1ec70104..d2e526434a 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -136,9 +136,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize needsRefresh = false reloading = true - tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(Section.Sensor.rawValue, Section.count - Section.Sensor.rawValue) - ), withRowAnimation: visible ? .Automatic : .None) - let calendar = NSCalendar.currentCalendar() let components = NSDateComponents() components.minute = 0 @@ -265,9 +262,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize private enum Section: Int { case Charts = 0 case Status - case Sensor - static let count = 3 + static let count = 2 } // MARK: - Chart Section Data @@ -367,32 +363,10 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize // MARK: - Pump/Sensor Section Data - private enum SensorRow: Int { - case State - - static let count = 1 - } - private lazy var emptyValueString: String = NSLocalizedString("––", comment: "The detail value of a numeric cell with no value" ) - private lazy var dateComponentsFormatter: NSDateComponentsFormatter = { - let formatter = NSDateComponentsFormatter() - formatter.unitsStyle = .Short - - return formatter - }() - - private lazy var numberFormatter = NSNumberFormatter() - - private lazy var dateFormatter: NSDateFormatter = { - let formatter = NSDateFormatter() - formatter.dateStyle = .MediumStyle - formatter.timeStyle = .MediumStyle - return formatter - }() - private lazy var timeFormatter: NSDateFormatter = { let formatter = NSDateFormatter() formatter.dateStyle = .NoStyle @@ -413,8 +387,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize return ChartRow.count case .Status: return StatusRow.count - case .Sensor: - return SensorRow.count } } @@ -483,18 +455,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } } - return cell - case .Sensor: - let cell = tableView.dequeueReusableCellWithIdentifier(UITableViewCell.className, forIndexPath: indexPath) - cell.selectionStyle = .None - - switch SensorRow(rawValue: indexPath.row)! { - case .State: - cell.textLabel?.text = NSLocalizedString("Sensor State", comment: "The title of the cell containing the current sensor state") - - cell.detailTextLabel?.text = dataManager.sensorInfo?.stateDescription ?? emptyValueString - } - return cell } } @@ -510,7 +470,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize case .IOB, .Dose, .COB: return 85 } - case .Status, .Sensor: + case .Status: return UITableViewAutomaticDimension } } @@ -548,10 +508,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } } } - case .Sensor: - if let URL = NSURL(string: "dexcomcgm://") { - UIApplication.sharedApplication().openURL(URL) - } } } diff --git a/Loop/Views/GlucoseHUDView.swift b/Loop/Views/GlucoseHUDView.swift index bcd05094a7..67b50840b1 100644 --- a/Loop/Views/GlucoseHUDView.swift +++ b/Loop/Views/GlucoseHUDView.swift @@ -15,27 +15,32 @@ final class GlucoseHUDView: HUDView { @IBOutlet private var unitLabel: UILabel! { didSet { - unitLabel?.text = "–" - unitLabel?.textColor = .glucoseTintColor + unitLabel.text = "–" + unitLabel.textColor = .glucoseTintColor } } @IBOutlet private var glucoseLabel: UILabel! { didSet { - glucoseLabel?.text = "–" - glucoseLabel?.textColor = .glucoseTintColor + glucoseLabel.text = "–" + glucoseLabel.textColor = .glucoseTintColor + } + } + + @IBOutlet private var alertLabel: UILabel! { + didSet { + alertLabel.alpha = 0 + alertLabel.backgroundColor = UIColor.agingColor + alertLabel.textColor = UIColor.whiteColor() + alertLabel.layer.cornerRadius = 9 + alertLabel.clipsToBounds = true } } func set(glucoseValue: GlucoseValue, for unit: HKUnit, from sensor: SensorDisplayable?) { caption?.text = timeFormatter.stringFromDate(glucoseValue.startDate) - let numberFormatter = NSNumberFormatter() - numberFormatter.numberStyle = .DecimalStyle - numberFormatter.minimumFractionDigits = unit.preferredMinimumFractionDigits - numberFormatter.usesSignificantDigits = true - numberFormatter.minimumSignificantDigits = 2 - numberFormatter.maximumSignificantDigits = 3 + let numberFormatter = NSNumberFormatter.glucoseFormatter(for: unit) glucoseLabel.text = numberFormatter.stringFromNumber(glucoseValue.quantity.doubleValueForUnit(unit)) var unitStrings = [unit.glucoseUnitDisplayString] @@ -45,6 +50,10 @@ final class GlucoseHUDView: HUDView { } unitLabel.text = unitStrings.joinWithSeparator(" ") + + UIView.animateWithDuration(0.25) { + self.alertLabel.alpha = sensor?.isStateValid == true ? 0 : 1 + } } private lazy var timeFormatter: NSDateFormatter = {