MockPumpManagerSettingsViewController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. final class MockPumpManagerSettingsViewController: UITableViewController {
  14. let pumpManager: MockPumpManager
  15. init(pumpManager: MockPumpManager) {
  16. self.pumpManager = pumpManager
  17. super.init(style: .grouped)
  18. }
  19. required init?(coder aDecoder: NSCoder) {
  20. fatalError("init(coder:) has not been implemented")
  21. }
  22. private let quantityFormatter = QuantityFormatter()
  23. override func viewDidLoad() {
  24. super.viewDidLoad()
  25. title = "Pump Settings"
  26. tableView.rowHeight = UITableView.automaticDimension
  27. tableView.estimatedRowHeight = 44
  28. tableView.sectionHeaderHeight = UITableView.automaticDimension
  29. tableView.estimatedSectionHeaderHeight = 55
  30. tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
  31. tableView.register(BoundSwitchTableViewCell.self, forCellReuseIdentifier: BoundSwitchTableViewCell.className)
  32. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  33. tableView.register(SuspendResumeTableViewCell.self, forCellReuseIdentifier: SuspendResumeTableViewCell.className)
  34. pumpManager.addStatusObserver(self, queue: .main)
  35. let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
  36. self.navigationItem.setRightBarButton(button, animated: false)
  37. }
  38. @objc func doneTapped(_ sender: Any) {
  39. done()
  40. }
  41. private func done() {
  42. if let nav = navigationController as? SettingsNavigationViewController {
  43. nav.notifyComplete()
  44. }
  45. if let nav = navigationController as? MockPumpManagerSetupViewController {
  46. nav.finishedSettingsDisplay()
  47. }
  48. }
  49. // MARK: - Data Source
  50. private enum Section: Int, CaseIterable {
  51. case actions = 0
  52. case settings
  53. case deletePump
  54. }
  55. private enum ActionRow: Int, CaseIterable {
  56. case suspendResume = 0
  57. }
  58. private enum SettingsRow: Int, CaseIterable {
  59. case reservoirRemaining = 0
  60. case batteryRemaining
  61. case tempBasalErrorToggle
  62. case bolusErrorToggle
  63. case suspendErrorToggle
  64. case resumeErrorToggle
  65. }
  66. // MARK: UITableViewDataSource
  67. override func numberOfSections(in tableView: UITableView) -> Int {
  68. return Section.allCases.count
  69. }
  70. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  71. switch Section(rawValue: section)! {
  72. case .actions:
  73. return ActionRow.allCases.count
  74. case .settings:
  75. return SettingsRow.allCases.count
  76. case .deletePump:
  77. return 1
  78. }
  79. }
  80. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  81. switch Section(rawValue: section)! {
  82. case .actions:
  83. return nil
  84. case .settings:
  85. return "Configuration"
  86. case .deletePump:
  87. return " " // Use an empty string for more dramatic spacing
  88. }
  89. }
  90. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  91. switch Section(rawValue: indexPath.section)! {
  92. case .actions:
  93. switch ActionRow(rawValue: indexPath.row)! {
  94. case .suspendResume:
  95. let cell = tableView.dequeueReusableCell(withIdentifier: SuspendResumeTableViewCell.className, for: indexPath) as! SuspendResumeTableViewCell
  96. cell.basalDeliveryState = pumpManager.status.basalDeliveryState
  97. return cell
  98. }
  99. case .settings:
  100. switch SettingsRow(rawValue: indexPath.row)! {
  101. case .reservoirRemaining:
  102. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  103. cell.textLabel?.text = "Reservoir Remaining"
  104. cell.detailTextLabel?.text = quantityFormatter.string(from: HKQuantity(unit: .internationalUnit(), doubleValue: pumpManager.state.reservoirUnitsRemaining), for: .internationalUnit())
  105. cell.accessoryType = .disclosureIndicator
  106. return cell
  107. case .batteryRemaining:
  108. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  109. cell.textLabel?.text = "Battery Remaining"
  110. if let remainingCharge = pumpManager.status.pumpBatteryChargeRemaining {
  111. cell.detailTextLabel?.text = "\(Int(round(remainingCharge * 100)))%"
  112. } else {
  113. cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString
  114. }
  115. cell.accessoryType = .disclosureIndicator
  116. return cell
  117. case .tempBasalErrorToggle:
  118. return switchTableViewCell(for: indexPath, titled: "Error on Temp Basal", boundTo: \.tempBasalEnactmentShouldError)
  119. case .bolusErrorToggle:
  120. return switchTableViewCell(for: indexPath, titled: "Error on Bolus", boundTo: \.bolusEnactmentShouldError)
  121. case .suspendErrorToggle:
  122. return switchTableViewCell(for: indexPath, titled: "Error on Suspend", boundTo: \.deliverySuspensionShouldError)
  123. case .resumeErrorToggle:
  124. return switchTableViewCell(for: indexPath, titled: "Error on Resume", boundTo: \.deliveryResumptionShouldError)
  125. }
  126. case .deletePump:
  127. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  128. cell.textLabel?.text = "Delete Pump"
  129. cell.textLabel?.textAlignment = .center
  130. cell.tintColor = .delete
  131. cell.isEnabled = true
  132. return cell
  133. }
  134. }
  135. private func switchTableViewCell(for indexPath: IndexPath, titled title: String, boundTo keyPath: WritableKeyPath<MockPumpManagerState, Bool>) -> SwitchTableViewCell {
  136. let cell = tableView.dequeueReusableCell(withIdentifier: BoundSwitchTableViewCell.className, for: indexPath) as! BoundSwitchTableViewCell
  137. cell.textLabel?.text = title
  138. cell.switch?.isOn = pumpManager.state[keyPath: keyPath]
  139. cell.onToggle = { [unowned pumpManager] isOn in
  140. pumpManager.state[keyPath: keyPath] = isOn
  141. }
  142. return cell
  143. }
  144. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  145. let sender = tableView.cellForRow(at: indexPath)
  146. switch Section(rawValue: indexPath.section)! {
  147. case .actions:
  148. switch ActionRow(rawValue: indexPath.row)! {
  149. case .suspendResume:
  150. if let suspendResumeCell = sender as? SuspendResumeTableViewCell {
  151. suspendResumeCellTapped(suspendResumeCell)
  152. }
  153. tableView.deselectRow(at: indexPath, animated: true)
  154. }
  155. case .settings:
  156. switch SettingsRow(rawValue: indexPath.row)! {
  157. case .reservoirRemaining:
  158. let vc = TextFieldTableViewController()
  159. vc.value = String(format: "%.1f", pumpManager.state.reservoirUnitsRemaining)
  160. vc.unit = "U"
  161. vc.keyboardType = .decimalPad
  162. vc.indexPath = indexPath
  163. vc.delegate = self
  164. show(vc, sender: sender)
  165. case .batteryRemaining:
  166. let vc = PercentageTextFieldTableViewController()
  167. vc.percentage = pumpManager.status.pumpBatteryChargeRemaining
  168. vc.indexPath = indexPath
  169. vc.percentageDelegate = self
  170. show(vc, sender: sender)
  171. case .tempBasalErrorToggle, .bolusErrorToggle, .suspendErrorToggle, .resumeErrorToggle:
  172. break
  173. }
  174. case .deletePump:
  175. let confirmVC = UIAlertController(pumpDeletionHandler: {
  176. self.pumpManager.notifyDelegateOfDeactivation {
  177. DispatchQueue.main.async {
  178. self.done()
  179. }
  180. }
  181. })
  182. present(confirmVC, animated: true) {
  183. tableView.deselectRow(at: indexPath, animated: true)
  184. }
  185. }
  186. }
  187. private func suspendResumeCellTapped(_ cell: SuspendResumeTableViewCell) {
  188. switch cell.shownAction {
  189. case .resume:
  190. pumpManager.resumeDelivery { (error) in
  191. if let error = error {
  192. DispatchQueue.main.async {
  193. let title = LocalizedString("Error Resuming", comment: "The alert title for a resume error")
  194. self.present(UIAlertController(with: error, title: title), animated: true)
  195. }
  196. }
  197. }
  198. case .suspend:
  199. pumpManager.suspendDelivery { (error) in
  200. if let error = error {
  201. DispatchQueue.main.async {
  202. let title = LocalizedString("Error Suspending", comment: "The alert title for a suspend error")
  203. self.present(UIAlertController(with: error, title: title), animated: true)
  204. }
  205. }
  206. }
  207. }
  208. }
  209. }
  210. extension MockPumpManagerSettingsViewController: PumpManagerStatusObserver {
  211. public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  212. dispatchPrecondition(condition: .onQueue(.main))
  213. if let suspendResumeTableViewCell = self.tableView?.cellForRow(at: IndexPath(row: ActionRow.suspendResume.rawValue, section: Section.actions.rawValue)) as? SuspendResumeTableViewCell
  214. {
  215. suspendResumeTableViewCell.basalDeliveryState = status.basalDeliveryState
  216. }
  217. }
  218. }
  219. extension MockPumpManagerSettingsViewController: TextFieldTableViewControllerDelegate {
  220. func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) {
  221. update(from: controller)
  222. }
  223. func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) {
  224. update(from: controller)
  225. }
  226. private func update(from controller: TextFieldTableViewController) {
  227. guard let indexPath = controller.indexPath else { assertionFailure(); return }
  228. assert(indexPath == [Section.settings.rawValue, SettingsRow.reservoirRemaining.rawValue])
  229. if let value = controller.value.flatMap(Double.init) {
  230. pumpManager.state.reservoirUnitsRemaining = value
  231. }
  232. tableView.reloadRows(at: [indexPath], with: .automatic)
  233. }
  234. }
  235. extension MockPumpManagerSettingsViewController: PercentageTextFieldTableViewControllerDelegate {
  236. func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) {
  237. guard let indexPath = controller.indexPath else { assertionFailure(); return }
  238. assert(indexPath == [Section.settings.rawValue, SettingsRow.batteryRemaining.rawValue])
  239. pumpManager.pumpBatteryChargeRemaining = controller.percentage.map { $0.clamped(to: 0...1) }
  240. tableView.reloadRows(at: [indexPath], with: .automatic)
  241. }
  242. }
  243. private extension UIAlertController {
  244. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  245. self.init(
  246. title: nil,
  247. message: "Are you sure you want to delete this pump?",
  248. preferredStyle: .actionSheet
  249. )
  250. addAction(UIAlertAction(
  251. title: "Delete Pump",
  252. style: .destructive,
  253. handler: { _ in handler() }
  254. ))
  255. addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
  256. }
  257. convenience init(title: String, error: Error) {
  258. let message: String
  259. if let localizedError = error as? LocalizedError {
  260. let sentenceFormat = NSLocalizedString("%@.", comment: "Appends a full-stop to a statement")
  261. message = [localizedError.failureReason, localizedError.recoverySuggestion].compactMap({ $0 }).map({
  262. String(format: sentenceFormat, $0)
  263. }).joined(separator: "\n")
  264. } else {
  265. message = String(describing: error)
  266. }
  267. self.init(
  268. title: title,
  269. message: message,
  270. preferredStyle: .alert
  271. )
  272. addAction(UIAlertAction(
  273. title: NSLocalizedString("OK", comment: "Button title to acknowledge error"),
  274. style: .default,
  275. handler: nil
  276. ))
  277. }
  278. }