MockPumpManagerSettingsViewController.swift 28 KB

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