| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- //
- // TransmitterSettingsViewController.swift
- // Loop
- //
- // Copyright © 2018 LoopKit Authors. All rights reserved.
- //
- import UIKit
- import Combine
- import HealthKit
- import LoopKit
- import LoopKitUI
- import CGMBLEKit
- import ShareClientUI
- class TransmitterSettingsViewController: UITableViewController {
- let cgmManager: TransmitterManager & CGMManagerUI
- private let displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
- private lazy var cancellables = Set<AnyCancellable>()
- private var glucoseUnit: HKUnit {
- displayGlucoseUnitObservable.displayGlucoseUnit
- }
- init(cgmManager: TransmitterManager & CGMManagerUI, displayGlucoseUnitObservable: DisplayGlucoseUnitObservable) {
- self.cgmManager = cgmManager
- self.displayGlucoseUnitObservable = displayGlucoseUnitObservable
- super.init(style: .grouped)
- cgmManager.addObserver(self, queue: .main)
- displayGlucoseUnitObservable.$displayGlucoseUnit
- .sink { [weak self] _ in self?.tableView.reloadData() }
- .store(in: &cancellables)
- }
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- title = cgmManager.localizedTitle
- tableView.rowHeight = UITableView.automaticDimension
- tableView.estimatedRowHeight = 44
- tableView.sectionHeaderHeight = UITableView.automaticDimension
- tableView.estimatedSectionHeaderHeight = 55
- tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
- tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
- tableView.register(SwitchTableViewCell.self, forCellReuseIdentifier: SwitchTableViewCell.className)
- let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
- self.navigationItem.setRightBarButton(button, animated: false)
- }
- @objc func doneTapped(_ sender: Any) {
- complete()
- }
- private func complete() {
- if let nav = navigationController as? SettingsNavigationViewController {
- nav.notifyComplete()
- }
- }
- override func viewWillAppear(_ animated: Bool) {
- if clearsSelectionOnViewWillAppear {
- // Manually invoke the delegate for rows deselecting on appear
- for indexPath in tableView.indexPathsForSelectedRows ?? [] {
- _ = tableView(tableView, willDeselectRowAt: indexPath)
- }
- }
- super.viewWillAppear(animated)
- }
- // MARK: - UITableViewDataSource
- private enum Section: Int, CaseIterable {
- case transmitterID
- case remoteDataSync
- case latestReading
- case latestCalibration
- case latestConnection
- case ages
- case share
- case delete
- }
- override func numberOfSections(in tableView: UITableView) -> Int {
- return Section.allCases.count
- }
- private enum LatestReadingRow: Int, CaseIterable {
- case glucose
- case date
- case trend
- case status
- }
- private enum LatestCalibrationRow: Int, CaseIterable {
- case glucose
- case date
- }
- private enum LatestConnectionRow: Int, CaseIterable {
- case date
- }
- private enum AgeRow: Int, CaseIterable {
- case sensorAge
- case sensorCountdown
- case sensorExpirationDate
- case transmitter
- }
- private enum ShareRow: Int, CaseIterable {
- case settings
- case openApp
- }
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- switch Section(rawValue: section)! {
- case .transmitterID:
- return 1
- case .remoteDataSync:
- return 1
- case .latestReading:
- return LatestReadingRow.allCases.count
- case .latestCalibration:
- return LatestCalibrationRow.allCases.count
- case .latestConnection:
- return LatestConnectionRow.allCases.count
- case .ages:
- return AgeRow.allCases.count
- case .share:
- return ShareRow.allCases.count
- case .delete:
- return 1
- }
- }
- private lazy var glucoseFormatter: QuantityFormatter = {
- let formatter = QuantityFormatter()
- formatter.setPreferredNumberFormatter(for: glucoseUnit)
- return formatter
- }()
- private lazy var dateFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .long
- formatter.timeStyle = .long
- formatter.doesRelativeDateFormatting = true
- return formatter
- }()
-
- private lazy var sensorExpirationFullFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .full
- formatter.timeStyle = .short
- formatter.doesRelativeDateFormatting = true
- //formatter.dateFormat = "E, MMM d 'at' h:mm a"
- return formatter
- }()
-
- private lazy var sensorExpirationRelativeFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .long
- formatter.timeStyle = .short
- formatter.doesRelativeDateFormatting = true
- return formatter
- }()
-
- private lazy var sensorExpAbsFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .long
- formatter.timeStyle = .short
- formatter.doesRelativeDateFormatting = false
- return formatter
- }()
-
- private lazy var sessionLengthFormatter: DateComponentsFormatter = {
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = [.day, .hour, .minute]
- formatter.unitsStyle = .full
- formatter.maximumUnitCount = 2
- return formatter
- }()
- private lazy var transmitterLengthFormatter: DateComponentsFormatter = {
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = [.day]
- formatter.unitsStyle = .full
- return formatter
- }()
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- switch Section(rawValue: indexPath.section)! {
- case .transmitterID:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell
- cell.textLabel?.text = LocalizedString("Transmitter ID", comment: "The title text for the Dexcom G5/G6 transmitter ID config value")
- cell.detailTextLabel?.text = cgmManager.transmitter.ID
- return cell
- case .remoteDataSync:
- let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell
- switchCell.selectionStyle = .none
- switchCell.switch?.isOn = cgmManager.shouldSyncToRemoteService
- switchCell.textLabel?.text = LocalizedString("Upload Readings", comment: "The title text for the upload glucose switch cell")
- switchCell.switch?.addTarget(self, action: #selector(uploadEnabledChanged(_:)), for: .valueChanged)
- return switchCell
- case .latestReading:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell
- let glucose = cgmManager.latestReading
- switch LatestReadingRow(rawValue: indexPath.row)! {
- case .glucose:
- cell.setGlucose(glucose?.glucose, unit: glucoseUnit, formatter: glucoseFormatter, isDisplayOnly: glucose?.isDisplayOnly ?? false)
- case .date:
- cell.setGlucoseDate(glucose?.readDate, formatter: dateFormatter)
- case .trend:
- cell.textLabel?.text = LocalizedString("Trend", comment: "Title describing glucose trend")
- if let trendRate = glucose?.trendRate {
- let glucoseUnitPerMinute = glucoseUnit.unitDivided(by: .minute())
- let trendPerMinute = HKQuantity(unit: glucoseUnit, doubleValue: trendRate.doubleValue(for: glucoseUnitPerMinute))
- if let formatted = glucoseFormatter.string(from: trendPerMinute, for: glucoseUnit) {
- cell.detailTextLabel?.text = String(format: LocalizedString("%@/min", comment: "Format string for glucose trend per minute. (1: glucose value and unit)"), formatted)
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- case .status:
- cell.textLabel?.text = LocalizedString("Status", comment: "Title describing CGM calibration and battery state")
- if let stateDescription = glucose?.stateDescription, !stateDescription.isEmpty {
- cell.detailTextLabel?.text = stateDescription
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- }
- return cell
- case .latestCalibration:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell
- let calibration = cgmManager.latestReading?.lastCalibration
- switch LatestCalibrationRow(rawValue: indexPath.row)! {
- case .glucose:
- cell.setGlucose(calibration?.glucose, unit: glucoseUnit, formatter: glucoseFormatter, isDisplayOnly: false)
- case .date:
- cell.setGlucoseDate(calibration?.date, formatter: dateFormatter)
- }
- return cell
- case .latestConnection:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell
- let connection = cgmManager.latestConnection
- switch LatestConnectionRow(rawValue: indexPath.row)! {
- case .date:
- cell.setGlucoseDate(connection, formatter: dateFormatter)
- cell.accessoryType = .disclosureIndicator
- }
- return cell
- case .ages:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell
- let glucose = cgmManager.latestReading
-
- switch AgeRow(rawValue: indexPath.row)! {
- case .sensorAge:
- cell.textLabel?.text = LocalizedString("Session Age", comment: "Title describing sensor session age")
-
- if let stateDescription = glucose?.stateDescription, !stateDescription.isEmpty && !stateDescription.contains("stopped") {
- if let sessionStart = cgmManager.latestReading?.sessionStartDate {
- cell.detailTextLabel?.text = sessionLengthFormatter.string(from: Date().timeIntervalSince(sessionStart))
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
-
- case .sensorCountdown:
- cell.textLabel?.text = LocalizedString("Sensor Expires", comment: "Title describing sensor sensor expiration")
-
- if let stateDescription = glucose?.stateDescription, !stateDescription.isEmpty && !stateDescription.contains("stopped") {
- if let sessionExp = cgmManager.latestReading?.sessionExpDate {
- let sessionCountDown = sessionExp.timeIntervalSince(Date())
- if sessionCountDown < 0 {
- cell.textLabel?.text = LocalizedString("Sensor Expired", comment: "Title describing past sensor sensor expiration")
- cell.detailTextLabel?.text = (sessionLengthFormatter.string(from: sessionCountDown * -1) ?? "") + " ago"
- } else {
- cell.detailTextLabel?.text = sessionLengthFormatter.string(from: sessionCountDown)
- }
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
-
- case .sensorExpirationDate:
- cell.textLabel?.text = ""
- if let stateDescription = glucose?.stateDescription, !stateDescription.isEmpty && !stateDescription.contains("stopped") {
- if let sessionExp = cgmManager.latestReading?.sessionExpDate {
- if sensorExpirationRelativeFormatter.string(from: sessionExp) == sensorExpAbsFormatter.string(from: sessionExp) {
- cell.detailTextLabel?.text = sensorExpirationFullFormatter.string(from: sessionExp)
- } else {
- cell.detailTextLabel?.text = sensorExpirationRelativeFormatter.string(from: sessionExp)
- }
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
-
- case .transmitter:
- cell.textLabel?.text = LocalizedString("Transmitter Age", comment: "Title describing transmitter session age")
- if let activation = cgmManager.latestReading?.activationDate {
- cell.detailTextLabel?.text = transmitterLengthFormatter.string(from: Date().timeIntervalSince(activation))
- } else {
- cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- }
- return cell
- case .share:
- switch ShareRow(rawValue: indexPath.row)! {
- case .settings:
- let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell
- let service = cgmManager.shareManager.shareService
- cell.textLabel?.text = service.title
- cell.detailTextLabel?.text = service.username ?? SettingsTableViewCell.TapToSetString
- cell.accessoryType = .disclosureIndicator
- return cell
- case .openApp:
- let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath)
- cell.textLabel?.text = LocalizedString("Open App", comment: "Button title to open CGM app")
- return cell
- }
- case .delete:
- let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
- cell.textLabel?.text = LocalizedString("Delete CGM", comment: "Title text for the button to remove a CGM from Loop")
- cell.textLabel?.textAlignment = .center
- cell.tintColor = .delete
- cell.isEnabled = true
- return cell
- }
- }
- override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
- switch Section(rawValue: section)! {
- case .transmitterID:
- return nil
- case .remoteDataSync:
- return LocalizedString("Remote Data Synchronization", comment: "Section title for remote data synchronization")
- case .latestReading:
- return LocalizedString("Latest Reading", comment: "Section title for latest glucose reading")
- case .latestCalibration:
- return LocalizedString("Latest Calibration", comment: "Section title for latest glucose calibration")
- case .latestConnection:
- return LocalizedString("Latest Connection", comment: "Section title for latest connection date")
- case .ages:
- return nil
- case .share:
- return nil
- case .delete:
- return " " // Use an empty string for more dramatic spacing
- }
- }
- override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
- switch Section(rawValue: indexPath.section)! {
- case .transmitterID:
- return false
- case .remoteDataSync:
- return false
- case .latestReading:
- return false
- case .latestCalibration:
- return false
- case .latestConnection:
- return true
- case .ages:
- return false
- case .share:
- return true
- case .delete:
- return true
- }
- }
- override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
- if self.tableView(tableView, shouldHighlightRowAt: indexPath) {
- return indexPath
- } else {
- return nil
- }
- }
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- switch Section(rawValue: indexPath.section)! {
- case .transmitterID:
- break
- case .remoteDataSync:
- break
- case .latestReading:
- break
- case .latestCalibration:
- break
- case .latestConnection:
- let vc = CommandResponseViewController(command: { (completionHandler) -> String in
- return String(reflecting: self.cgmManager)
- })
- vc.title = self.title
- show(vc, sender: nil)
- case .ages:
- break
- case .share:
- switch ShareRow(rawValue: indexPath.row)! {
- case .settings:
- let vc = ShareClientSettingsViewController(cgmManager: cgmManager.shareManager, displayGlucoseUnitObservable: displayGlucoseUnitObservable, allowsDeletion: false)
- show(vc, sender: nil)
- return // Don't deselect
- case .openApp:
- if let appURL = URL(string: "dexcomg6://") {
- UIApplication.shared.open(appURL)
- }
- }
- case .delete:
- let confirmVC = UIAlertController(cgmDeletionHandler: {
- self.cgmManager.notifyDelegateOfDeletion {
- DispatchQueue.main.async {
- self.complete()
- }
- }
- })
- present(confirmVC, animated: true) {
- tableView.deselectRow(at: indexPath, animated: true)
- }
- }
- tableView.deselectRow(at: indexPath, animated: true)
- }
- override func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
- switch Section(rawValue: indexPath.section)! {
- case .transmitterID:
- break
- case .remoteDataSync:
- break
- case .latestReading:
- break
- case .latestCalibration:
- break
- case .latestConnection:
- break
- case .ages:
- break
- case .share:
- switch ShareRow(rawValue: indexPath.row)! {
- case .settings:
- tableView.reloadRows(at: [indexPath], with: .fade)
- case .openApp:
- break
- }
- case .delete:
- break
- }
- return indexPath
- }
-
- @objc private func uploadEnabledChanged(_ sender: UISwitch) {
- cgmManager.shouldSyncToRemoteService = sender.isOn
- }
- }
- extension TransmitterSettingsViewController: TransmitterManagerObserver {
- func transmitterManagerDidUpdateLatestReading(_ manager: TransmitterManager) {
- tableView.reloadData()
- }
- }
- private extension UIAlertController {
- convenience init(cgmDeletionHandler handler: @escaping () -> Void) {
- self.init(
- title: nil,
- message: LocalizedString("Are you sure you want to delete this CGM?", comment: "Confirmation message for deleting a CGM"),
- preferredStyle: .actionSheet
- )
- addAction(UIAlertAction(
- title: LocalizedString("Delete CGM", comment: "Button title to delete CGM"),
- style: .destructive,
- handler: { (_) in
- handler()
- }
- ))
- let cancel = LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet")
- addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
- }
- }
- private extension SettingsTableViewCell {
- func setGlucose(_ glucose: HKQuantity?, unit: HKUnit, formatter: QuantityFormatter, isDisplayOnly: Bool) {
- if isDisplayOnly {
- textLabel?.text = LocalizedString("Glucose (Adjusted)", comment: "Describes a glucose value adjusted to reflect a recent calibration")
- } else {
- textLabel?.text = LocalizedString("Glucose", comment: "Title describing glucose value")
- }
- if let quantity = glucose, let formatted = formatter.string(from: quantity, for: unit) {
- detailTextLabel?.text = formatted
- } else {
- detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- }
- func setGlucoseDate(_ date: Date?, formatter: DateFormatter) {
- textLabel?.text = LocalizedString("Date", comment: "Title describing glucose date")
- if let date = date {
- detailTextLabel?.text = formatter.string(from: date)
- } else {
- detailTextLabel?.text = SettingsTableViewCell.NoValueString
- }
- }
- }
|