MockPumpManagerSettingsViewController.swift 30 KB

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