MockPumpManagerSettingsViewController.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. //
  2. // MockPumpManagerSettingsViewController.swift
  3. // LoopKitUI
  4. //
  5. // Created by Michael Pangburn on 11/20/18.
  6. // Copyright © 2018 LoopKit Authors. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. import LoopKitUI
  12. import MockKit
  13. import SwiftUI
  14. final class MockPumpManagerSettingsViewController: UITableViewController {
  15. let pumpManager: MockPumpManager
  16. let supportedInsulinTypes: [InsulinType]
  17. init(pumpManager: MockPumpManager, supportedInsulinTypes: [InsulinType]) {
  18. self.pumpManager = pumpManager
  19. self.supportedInsulinTypes = supportedInsulinTypes
  20. super.init(style: .grouped)
  21. title = NSLocalizedString("Pump Settings", comment: "Title for Pump simulator settings")
  22. }
  23. required init?(coder aDecoder: NSCoder) {
  24. fatalError("init(coder:) has not been implemented")
  25. }
  26. private let quantityFormatter = QuantityFormatter()
  27. override func viewDidLoad() {
  28. super.viewDidLoad()
  29. tableView.rowHeight = UITableView.automaticDimension
  30. tableView.estimatedRowHeight = 44
  31. tableView.sectionHeaderHeight = UITableView.automaticDimension
  32. tableView.estimatedSectionHeaderHeight = 55
  33. tableView.register(DateAndDurationTableViewCell.nib(), forCellReuseIdentifier: DateAndDurationTableViewCell.className)
  34. tableView.register(SegmentedControlTableViewCell.self, forCellReuseIdentifier: SegmentedControlTableViewCell.className)
  35. tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
  36. tableView.register(BoundSwitchTableViewCell.self, forCellReuseIdentifier: BoundSwitchTableViewCell.className)
  37. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  38. tableView.register(SuspendResumeTableViewCell.self, forCellReuseIdentifier: SuspendResumeTableViewCell.className)
  39. pumpManager.addStatusObserver(self, queue: .main)
  40. let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
  41. self.navigationItem.setRightBarButton(button, animated: false)
  42. }
  43. @objc func doneTapped(_ sender: Any) {
  44. done()
  45. }
  46. private func done() {
  47. if let nav = navigationController as? SettingsNavigationViewController {
  48. nav.notifyComplete()
  49. }
  50. if let nav = navigationController as? MockPumpManagerSetupViewController {
  51. nav.finishedSettingsDisplay()
  52. }
  53. }
  54. // MARK: - Data Source
  55. private enum Section: Int, CaseIterable {
  56. case basalRate = 0
  57. case actions
  58. case settings
  59. case statusProgress
  60. case deletePump
  61. }
  62. private enum ActionRow: Int, CaseIterable {
  63. case suspendResume = 0
  64. case occlusion
  65. case pumpError
  66. }
  67. private enum SettingsRow: Int, CaseIterable {
  68. case deliverableIncrements = 0
  69. case supportedBasalRates
  70. case supportedBolusVolumes
  71. case insulinType
  72. case reservoirRemaining
  73. case batteryRemaining
  74. case tempBasalErrorToggle
  75. case bolusErrorToggle
  76. case bolusCancelErrorToggle
  77. case suspendErrorToggle
  78. case resumeErrorToggle
  79. case uncertainDeliveryErrorToggle
  80. case lastReconciliationDate
  81. }
  82. private enum StatusProgressRow: Int, CaseIterable {
  83. case percentComplete
  84. case warningThreshold
  85. case criticalThreshold
  86. }
  87. // MARK: UITableViewDataSource
  88. override func numberOfSections(in tableView: UITableView) -> Int {
  89. return Section.allCases.count
  90. }
  91. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  92. switch Section(rawValue: section)! {
  93. case .basalRate:
  94. return 1
  95. case .actions:
  96. return ActionRow.allCases.count
  97. case .settings:
  98. return SettingsRow.allCases.count
  99. case .statusProgress:
  100. return StatusProgressRow.allCases.count
  101. case .deletePump:
  102. return 1
  103. }
  104. }
  105. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  106. switch Section(rawValue: section)! {
  107. case .basalRate:
  108. return nil
  109. case .actions:
  110. return nil
  111. case .settings:
  112. return "Configuration"
  113. case .statusProgress:
  114. return "Status Progress"
  115. case .deletePump:
  116. return " " // Use an empty string for more dramatic spacing
  117. }
  118. }
  119. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  120. switch Section(rawValue: indexPath.section)! {
  121. case .basalRate:
  122. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  123. cell.textLabel?.text = "Current Basal Rate"
  124. if let currentBasalRate = pumpManager.currentBasalRate {
  125. cell.detailTextLabel?.text = quantityFormatter.string(from: currentBasalRate, for: HKUnit.internationalUnit().unitDivided(by: .hour()))
  126. } else {
  127. cell.detailTextLabel?.text = "—"
  128. }
  129. cell.isUserInteractionEnabled = false
  130. return cell
  131. case .actions:
  132. switch ActionRow(rawValue: indexPath.row)! {
  133. case .suspendResume:
  134. let cell = tableView.dequeueReusableCell(withIdentifier: SuspendResumeTableViewCell.className, for: indexPath) as! SuspendResumeTableViewCell
  135. cell.basalDeliveryState = pumpManager.status.basalDeliveryState
  136. return cell
  137. case .occlusion:
  138. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  139. if pumpManager.state.occlusionDetected {
  140. cell.textLabel?.text = "Resolve Occlusion"
  141. } else {
  142. cell.textLabel?.text = "Detect Occlusion"
  143. }
  144. return cell
  145. case .pumpError:
  146. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  147. if pumpManager.state.pumpErrorDetected {
  148. cell.textLabel?.text = "Resolve Pump Error"
  149. } else {
  150. cell.textLabel?.text = "Cause Pump Error"
  151. }
  152. return cell
  153. }
  154. case .settings:
  155. switch SettingsRow(rawValue: indexPath.row)! {
  156. case .deliverableIncrements:
  157. let cell = tableView.dequeueReusableCell(withIdentifier: SegmentedControlTableViewCell.className, for: indexPath) as! SegmentedControlTableViewCell
  158. let possibleDeliverableIncrements = MockPumpManagerState.DeliverableIncrements.allCases
  159. cell.textLabel?.text = "Increments"
  160. cell.options = possibleDeliverableIncrements.map { increments in
  161. switch increments {
  162. case .omnipod:
  163. return "Pod"
  164. case .medtronicX22:
  165. return "x22"
  166. case .medtronicX23:
  167. return "x23"
  168. case .custom:
  169. return "Custom"
  170. }
  171. }
  172. cell.segmentedControl.selectedSegmentIndex = possibleDeliverableIncrements.firstIndex(of: pumpManager.state.deliverableIncrements)!
  173. cell.onSelection { [pumpManager] index in
  174. pumpManager.state.deliverableIncrements = possibleDeliverableIncrements[index]
  175. tableView.reloadRows(at: [IndexPath(row: SettingsRow.supportedBasalRates.rawValue, section: Section.settings.rawValue), IndexPath(row: SettingsRow.supportedBolusVolumes.rawValue, section: Section.settings.rawValue)], with: .automatic)
  176. }
  177. return cell
  178. case .supportedBasalRates:
  179. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  180. cell.textLabel?.text = "Basal Rates"
  181. cell.detailTextLabel?.text = pumpManager.state.supportedBasalRatesDescription
  182. if pumpManager.state.deliverableIncrements == .custom {
  183. cell.accessoryType = .disclosureIndicator
  184. }
  185. return cell
  186. case .supportedBolusVolumes:
  187. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  188. cell.textLabel?.text = "Bolus Volumes"
  189. cell.detailTextLabel?.text = pumpManager.state.supportedBolusVolumesDescription
  190. if pumpManager.state.deliverableIncrements == .custom {
  191. cell.accessoryType = .disclosureIndicator
  192. }
  193. return cell
  194. case .insulinType:
  195. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  196. cell.prepareForReuse()
  197. cell.textLabel?.text = "Insulin Type"
  198. cell.detailTextLabel?.text = pumpManager.state.insulinType.brandName
  199. cell.accessoryType = .disclosureIndicator
  200. return cell
  201. case .reservoirRemaining:
  202. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  203. cell.textLabel?.text = "Reservoir Remaining"
  204. cell.detailTextLabel?.text = quantityFormatter.string(from: HKQuantity(unit: .internationalUnit(), doubleValue: pumpManager.state.reservoirUnitsRemaining), for: .internationalUnit())
  205. cell.accessoryType = .disclosureIndicator
  206. return cell
  207. case .batteryRemaining:
  208. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  209. cell.textLabel?.text = "Battery Remaining"
  210. if let remainingCharge = pumpManager.status.pumpBatteryChargeRemaining {
  211. cell.detailTextLabel?.text = "\(Int(round(remainingCharge * 100)))%"
  212. } else {
  213. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  214. }
  215. cell.accessoryType = .disclosureIndicator
  216. return cell
  217. case .tempBasalErrorToggle:
  218. return switchTableViewCell(for: indexPath, titled: "Error on Temp Basal", boundTo: \.tempBasalEnactmentShouldError)
  219. case .bolusErrorToggle:
  220. return switchTableViewCell(for: indexPath, titled: "Error on Bolus", boundTo: \.bolusEnactmentShouldError)
  221. case .bolusCancelErrorToggle:
  222. return switchTableViewCell(for: indexPath, titled: "Error on Cancel Bolus", boundTo: \.bolusCancelShouldError)
  223. case .suspendErrorToggle:
  224. return switchTableViewCell(for: indexPath, titled: "Error on Suspend", boundTo: \.deliverySuspensionShouldError)
  225. case .resumeErrorToggle:
  226. return switchTableViewCell(for: indexPath, titled: "Error on Resume", boundTo: \.deliveryResumptionShouldError)
  227. case .uncertainDeliveryErrorToggle:
  228. return switchTableViewCell(for: indexPath, titled: "Next Delivery Command Uncertain", boundTo: \.deliveryCommandsShouldTriggerUncertainDelivery)
  229. case .lastReconciliationDate:
  230. let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className, for: indexPath) as! DateAndDurationTableViewCell
  231. cell.titleLabel.text = "Last Reconciliation Date"
  232. cell.date = pumpManager.lastReconciliation ?? Date()
  233. cell.datePicker.maximumDate = Date()
  234. cell.datePicker.minimumDate = Date() - .hours(48)
  235. cell.datePicker.datePickerMode = .dateAndTime
  236. #if swift(>=5.2)
  237. if #available(iOS 14.0, *) {
  238. cell.datePicker.preferredDatePickerStyle = .wheels
  239. }
  240. #endif
  241. cell.datePicker.isEnabled = true
  242. cell.delegate = self
  243. return cell
  244. }
  245. case .statusProgress:
  246. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  247. switch StatusProgressRow(rawValue: indexPath.row)! {
  248. case .percentComplete:
  249. cell.textLabel?.text = "Percent Completed"
  250. if let percentCompleted = pumpManager.state.progressPercentComplete {
  251. cell.detailTextLabel?.text = "\(Int(round(percentCompleted * 100)))%"
  252. } else {
  253. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  254. }
  255. case .warningThreshold:
  256. cell.textLabel?.text = "Warning Threshold"
  257. if let warningThreshold = pumpManager.state.progressWarningThresholdPercentValue {
  258. cell.detailTextLabel?.text = "\(Int(round(warningThreshold * 100)))%"
  259. } else {
  260. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  261. }
  262. case .criticalThreshold:
  263. cell.textLabel?.text = "Critical Threshold"
  264. if let criticalThreshold = pumpManager.state.progressCriticalThresholdPercentValue {
  265. cell.detailTextLabel?.text = "\(Int(round(criticalThreshold * 100)))%"
  266. } else {
  267. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  268. }
  269. }
  270. cell.accessoryType = .disclosureIndicator
  271. return cell
  272. case .deletePump:
  273. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  274. cell.textLabel?.text = "Delete Pump"
  275. cell.textLabel?.textAlignment = .center
  276. cell.tintColor = .delete
  277. cell.isEnabled = true
  278. return cell
  279. }
  280. }
  281. private func switchTableViewCell(for indexPath: IndexPath, titled title: String, boundTo keyPath: WritableKeyPath<MockPumpManagerState, Bool>) -> SwitchTableViewCell {
  282. let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
  283. cell.textLabel?.text = title
  284. cell.switch?.isOn = pumpManager.state[keyPath: keyPath]
  285. cell.onToggle = { [unowned pumpManager] isOn in
  286. pumpManager.state[keyPath: keyPath] = isOn
  287. }
  288. return cell
  289. }
  290. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  291. let sender = tableView.cellForRow(at: indexPath)
  292. switch Section(rawValue: indexPath.section)! {
  293. case .actions:
  294. switch ActionRow(rawValue: indexPath.row)! {
  295. case .suspendResume:
  296. if let suspendResumeCell = sender as? SuspendResumeTableViewCell {
  297. suspendResumeCellTapped(suspendResumeCell)
  298. }
  299. tableView.deselectRow(at: indexPath, animated: true)
  300. case .occlusion:
  301. pumpManager.state.occlusionDetected = !pumpManager.state.occlusionDetected
  302. tableView.deselectRow(at: indexPath, animated: true)
  303. tableView.reloadRows(at: [indexPath], with: .automatic)
  304. case .pumpError:
  305. pumpManager.state.pumpErrorDetected = !pumpManager.state.pumpErrorDetected
  306. tableView.deselectRow(at: indexPath, animated: true)
  307. tableView.reloadRows(at: [indexPath], with: .automatic)
  308. }
  309. case .settings:
  310. tableView.deselectRow(at: indexPath, animated: true)
  311. switch SettingsRow(rawValue: indexPath.row)! {
  312. case .deliverableIncrements:
  313. break
  314. case .supportedBasalRates:
  315. if pumpManager.state.deliverableIncrements == .custom, pumpManager.state.supportedBasalRates.indices.contains(1) {
  316. let basalRates = pumpManager.state.supportedBasalRates
  317. let vc = SupportedRangeTableViewController(minValue: basalRates.first!, maxValue: basalRates.last!, stepSize: basalRates[1] - basalRates.first!)
  318. vc.title = "Supported Basal Rates"
  319. vc.indexPath = indexPath
  320. vc.delegate = self
  321. show(vc, sender: sender)
  322. }
  323. break
  324. case .supportedBolusVolumes:
  325. if pumpManager.state.deliverableIncrements == .custom, pumpManager.state.supportedBolusVolumes.indices.contains(1) {
  326. let bolusVolumes = pumpManager.state.supportedBolusVolumes
  327. let vc = SupportedRangeTableViewController(minValue: bolusVolumes.first!, maxValue: bolusVolumes.last!, stepSize: bolusVolumes[1] - bolusVolumes.first!)
  328. vc.title = "Supported Bolus Volumes"
  329. vc.indexPath = indexPath
  330. vc.delegate = self
  331. show(vc, sender: sender)
  332. }
  333. break
  334. case .insulinType:
  335. let view = InsulinTypeSetting(initialValue: pumpManager.state.insulinType, supportedInsulinTypes: InsulinType.allCases) { (newType) in
  336. self.pumpManager.state.insulinType = newType
  337. }
  338. let vc = DismissibleHostingController(rootView: view) {
  339. tableView.reloadRows(at: [indexPath], with: .automatic)
  340. }
  341. vc.title = LocalizedString("Insulin Type", comment: "Controller title for insulin type selection screen")
  342. show(vc, sender: sender)
  343. case .reservoirRemaining:
  344. let vc = TextFieldTableViewController()
  345. vc.value = String(format: "%.1f", pumpManager.state.reservoirUnitsRemaining)
  346. vc.unit = "U"
  347. vc.keyboardType = .decimalPad
  348. vc.indexPath = indexPath
  349. vc.delegate = self
  350. show(vc, sender: sender)
  351. case .batteryRemaining:
  352. let vc = PercentageTextFieldTableViewController()
  353. vc.percentage = pumpManager.status.pumpBatteryChargeRemaining
  354. vc.indexPath = indexPath
  355. vc.percentageDelegate = self
  356. show(vc, sender: sender)
  357. case .tempBasalErrorToggle, .bolusErrorToggle, .bolusCancelErrorToggle, .suspendErrorToggle, .resumeErrorToggle, .uncertainDeliveryErrorToggle:
  358. break
  359. case .lastReconciliationDate:
  360. tableView.deselectRow(at: indexPath, animated: true)
  361. tableView.beginUpdates()
  362. tableView.endUpdates()
  363. }
  364. case .statusProgress:
  365. let vc = PercentageTextFieldTableViewController()
  366. vc.indexPath = indexPath
  367. vc.percentageDelegate = self
  368. switch StatusProgressRow(rawValue: indexPath.row)! {
  369. case .percentComplete:
  370. vc.percentage = pumpManager.state.progressPercentComplete
  371. case .warningThreshold:
  372. vc.percentage = pumpManager.state.progressWarningThresholdPercentValue
  373. case .criticalThreshold:
  374. vc.percentage = pumpManager.state.progressCriticalThresholdPercentValue
  375. }
  376. show(vc, sender: sender)
  377. case .deletePump:
  378. let confirmVC = UIAlertController(pumpDeletionHandler: {
  379. self.pumpManager.notifyDelegateOfDeactivation {
  380. DispatchQueue.main.async {
  381. self.done()
  382. }
  383. }
  384. })
  385. present(confirmVC, animated: true) {
  386. tableView.deselectRow(at: indexPath, animated: true)
  387. }
  388. default:
  389. tableView.deselectRow(at: indexPath, animated: true)
  390. }
  391. }
  392. private func suspendResumeCellTapped(_ cell: SuspendResumeTableViewCell) {
  393. switch cell.shownAction {
  394. case .resume:
  395. pumpManager.resumeDelivery { (error) in
  396. if let error = error {
  397. DispatchQueue.main.async {
  398. let title = LocalizedString("Error Resuming", comment: "The alert title for a resume error")
  399. self.present(UIAlertController(with: error, title: title), animated: true)
  400. }
  401. }
  402. }
  403. case .suspend:
  404. pumpManager.suspendDelivery { (error) in
  405. if let error = error {
  406. DispatchQueue.main.async {
  407. let title = LocalizedString("Error Suspending", comment: "The alert title for a suspend error")
  408. self.present(UIAlertController(with: error, title: title), animated: true)
  409. }
  410. }
  411. }
  412. default:
  413. break
  414. }
  415. }
  416. override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
  417. switch Section(rawValue: indexPath.section)! {
  418. case .settings:
  419. switch SettingsRow(rawValue: indexPath.row)! {
  420. case .lastReconciliationDate:
  421. let resetAction = UIContextualAction(style: .normal, title: "Reset") {[weak self] _,_,_ in
  422. self?.pumpManager.testLastReconciliation = nil
  423. tableView.reloadRows(at: [indexPath], with: .automatic)
  424. }
  425. resetAction.backgroundColor = .systemRed
  426. return UISwipeActionsConfiguration(actions: [resetAction])
  427. default:
  428. break
  429. }
  430. default:
  431. break
  432. }
  433. return nil
  434. }
  435. }
  436. extension MockPumpManagerSettingsViewController: DatePickerTableViewCellDelegate {
  437. func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
  438. guard let row = tableView.indexPath(for: cell)?.row else { return }
  439. switch SettingsRow(rawValue: row) {
  440. case .lastReconciliationDate?:
  441. pumpManager.testLastReconciliation = cell.date
  442. default:
  443. break
  444. }
  445. }
  446. }
  447. extension MockPumpManagerSettingsViewController: PumpManagerStatusObserver {
  448. public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  449. dispatchPrecondition(condition: .onQueue(.main))
  450. if let suspendResumeTableViewCell = self.tableView?.cellForRow(at: IndexPath(row: ActionRow.suspendResume.rawValue, section: Section.actions.rawValue)) as? SuspendResumeTableViewCell
  451. {
  452. suspendResumeTableViewCell.basalDeliveryState = status.basalDeliveryState
  453. }
  454. tableView.reloadSections([Section.basalRate.rawValue], with: .automatic)
  455. }
  456. }
  457. extension MockPumpManagerSettingsViewController: TextFieldTableViewControllerDelegate {
  458. func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) {
  459. update(from: controller)
  460. }
  461. func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) {
  462. update(from: controller)
  463. }
  464. private func update(from controller: TextFieldTableViewController) {
  465. guard let indexPath = controller.indexPath else { assertionFailure(); return }
  466. assert(indexPath == [Section.settings.rawValue, SettingsRow.reservoirRemaining.rawValue])
  467. if let value = controller.value.flatMap(Double.init) {
  468. pumpManager.state.reservoirUnitsRemaining = max(value, 0)
  469. }
  470. tableView.reloadRows(at: [indexPath], with: .automatic)
  471. }
  472. }
  473. extension MockPumpManagerSettingsViewController: PercentageTextFieldTableViewControllerDelegate {
  474. func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) {
  475. guard let indexPath = controller.indexPath else {
  476. assertionFailure()
  477. return
  478. }
  479. switch indexPath {
  480. case [Section.settings.rawValue, SettingsRow.batteryRemaining.rawValue]:
  481. pumpManager.pumpBatteryChargeRemaining = controller.percentage.map { $0.clamped(to: 0...1) }
  482. tableView.reloadRows(at: [indexPath], with: .automatic)
  483. case [Section.statusProgress.rawValue, StatusProgressRow.percentComplete.rawValue]:
  484. pumpManager.state.progressPercentComplete = controller.percentage.map { $0.clamped(to: 0...1) }
  485. tableView.reloadRows(at: [indexPath], with: .automatic)
  486. case [Section.statusProgress.rawValue, StatusProgressRow.warningThreshold.rawValue]:
  487. pumpManager.state.progressWarningThresholdPercentValue = controller.percentage.map { $0.clamped(to: 0...1) }
  488. tableView.reloadRows(at: [indexPath], with: .automatic)
  489. case [Section.statusProgress.rawValue, StatusProgressRow.criticalThreshold.rawValue]:
  490. pumpManager.state.progressCriticalThresholdPercentValue = controller.percentage.map { $0.clamped(to: 0...1) }
  491. tableView.reloadRows(at: [indexPath], with: .automatic)
  492. default:
  493. assertionFailure()
  494. }
  495. }
  496. }
  497. private extension UIAlertController {
  498. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  499. self.init(
  500. title: nil,
  501. message: "Are you sure you want to delete this pump?",
  502. preferredStyle: .actionSheet
  503. )
  504. addAction(UIAlertAction(
  505. title: "Delete Pump",
  506. style: .destructive,
  507. handler: { _ in handler() }
  508. ))
  509. addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
  510. }
  511. convenience init(title: String, error: Error) {
  512. let message: String
  513. if let localizedError = error as? LocalizedError {
  514. let sentenceFormat = NSLocalizedString("%@.", comment: "Appends a full-stop to a statement")
  515. message = [localizedError.failureReason, localizedError.recoverySuggestion].compactMap({ $0 }).map({
  516. String(format: sentenceFormat, $0)
  517. }).joined(separator: "\n")
  518. } else {
  519. message = String(describing: error)
  520. }
  521. self.init(
  522. title: title,
  523. message: message,
  524. preferredStyle: .alert
  525. )
  526. addAction(UIAlertAction(
  527. title: NSLocalizedString("OK", comment: "Button title to acknowledge error"),
  528. style: .default,
  529. handler: nil
  530. ))
  531. }
  532. }
  533. extension MockPumpManagerSettingsViewController: SupportedRangeTableViewControllerDelegate {
  534. func supportedRangeDidUpdate(_ controller: SupportedRangeTableViewController) {
  535. guard let indexPath = controller.indexPath else {
  536. assertionFailure()
  537. return
  538. }
  539. let rangeMin = Int(controller.minValue/controller.stepSize)
  540. let rangeMax = Int(controller.maxValue/controller.stepSize)
  541. let rangeStep = 1/controller.stepSize
  542. let values: [Double] = (rangeMin...rangeMax).map { Double($0) / rangeStep }
  543. switch indexPath {
  544. case [Section.settings.rawValue, SettingsRow.supportedBasalRates.rawValue]:
  545. pumpManager.state.supportedBasalRates = values
  546. tableView.reloadRows(at: [indexPath], with: .automatic)
  547. case [Section.settings.rawValue, SettingsRow.supportedBolusVolumes.rawValue]:
  548. pumpManager.state.supportedBolusVolumes = values
  549. tableView.reloadRows(at: [indexPath], with: .automatic)
  550. default:
  551. assertionFailure()
  552. }
  553. }
  554. }