MinimedPumpSettingsViewController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. //
  2. // MinimedPumpSettingsViewController.swift
  3. // Loop
  4. //
  5. // Copyright © 2018 LoopKit Authors. All rights reserved.
  6. //
  7. import UIKit
  8. import LoopKitUI
  9. import MinimedKit
  10. import RileyLinkKitUI
  11. import LoopKit
  12. class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
  13. let pumpManager: MinimedPumpManager
  14. init(pumpManager: MinimedPumpManager) {
  15. self.pumpManager = pumpManager
  16. super.init(rileyLinkPumpManager: pumpManager, devicesSectionIndex: Section.rileyLinks.rawValue, style: .grouped)
  17. }
  18. required init?(coder aDecoder: NSCoder) {
  19. fatalError("init(coder:) has not been implemented")
  20. }
  21. override func viewDidLoad() {
  22. super.viewDidLoad()
  23. title = LocalizedString("Pump Settings", comment: "Title of the pump settings view controller")
  24. tableView.rowHeight = UITableView.automaticDimension
  25. tableView.estimatedRowHeight = 44
  26. tableView.sectionHeaderHeight = UITableView.automaticDimension
  27. tableView.estimatedSectionHeaderHeight = 55
  28. tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
  29. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  30. tableView.register(SuspendResumeTableViewCell.self, forCellReuseIdentifier: SuspendResumeTableViewCell.className)
  31. let imageView = UIImageView(image: pumpManager.state.largePumpImage)
  32. imageView.contentMode = .bottom
  33. imageView.frame.size.height += 18 // feels right
  34. tableView.tableHeaderView = imageView
  35. pumpManager.addStatusObserver(self, queue: .main)
  36. let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
  37. self.navigationItem.setRightBarButton(button, animated: false)
  38. }
  39. @objc func doneTapped(_ sender: Any) {
  40. done()
  41. }
  42. private func done() {
  43. if let nav = navigationController as? SettingsNavigationViewController {
  44. nav.notifyComplete()
  45. }
  46. if let nav = navigationController as? MinimedPumpManagerSetupViewController {
  47. nav.finishedSettingsDisplay()
  48. }
  49. }
  50. override func viewWillAppear(_ animated: Bool) {
  51. if clearsSelectionOnViewWillAppear {
  52. // Manually invoke the delegate for rows deselecting on appear
  53. for indexPath in tableView.indexPathsForSelectedRows ?? [] {
  54. _ = tableView(tableView, willDeselectRowAt: indexPath)
  55. }
  56. }
  57. super.viewWillAppear(animated)
  58. }
  59. // MARK: - Data Source
  60. private enum Section: Int, CaseIterable {
  61. case info = 0
  62. case actions
  63. case settings
  64. case rileyLinks
  65. case delete
  66. }
  67. private enum InfoRow: Int, CaseIterable {
  68. case pumpID = 0
  69. case pumpModel
  70. case pumpFirmware
  71. case pumpRegion
  72. }
  73. private enum ActionsRow: Int, CaseIterable {
  74. case suspendResume = 0
  75. }
  76. private enum SettingsRow: Int, CaseIterable {
  77. case timeZoneOffset = 0
  78. case batteryChemistry
  79. case preferredInsulinDataSource
  80. case insulinType
  81. }
  82. // MARK: UITableViewDataSource
  83. override func numberOfSections(in tableView: UITableView) -> Int {
  84. return Section.allCases.count
  85. }
  86. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  87. switch Section(rawValue: section)! {
  88. case .info:
  89. return InfoRow.allCases.count
  90. case .actions:
  91. return ActionsRow.allCases.count
  92. case .settings:
  93. return SettingsRow.allCases.count
  94. case .rileyLinks:
  95. return super.tableView(tableView, numberOfRowsInSection: section)
  96. case .delete:
  97. return 1
  98. }
  99. }
  100. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  101. switch Section(rawValue: section)! {
  102. case .settings:
  103. return LocalizedString("Configuration", comment: "The title of the configuration section in settings")
  104. case .rileyLinks:
  105. return super.tableView(tableView, titleForHeaderInSection: section)
  106. case .delete:
  107. return " " // Use an empty string for more dramatic spacing
  108. case .info, .actions:
  109. return nil
  110. }
  111. }
  112. override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  113. switch Section(rawValue: section)! {
  114. case .rileyLinks:
  115. return super.tableView(tableView, viewForHeaderInSection: section)
  116. case .info, .settings, .delete, .actions:
  117. return nil
  118. }
  119. }
  120. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  121. switch Section(rawValue: indexPath.section)! {
  122. case .info:
  123. switch InfoRow(rawValue: indexPath.row)! {
  124. case .pumpID:
  125. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  126. cell.textLabel?.text = LocalizedString("Pump ID", comment: "The title text for the pump ID config value")
  127. cell.detailTextLabel?.text = pumpManager.state.pumpID
  128. return cell
  129. case .pumpModel:
  130. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  131. cell.textLabel?.text = LocalizedString("Pump Model", comment: "The title of the cell showing the pump model number")
  132. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpModel)
  133. return cell
  134. case .pumpFirmware:
  135. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  136. cell.textLabel?.text = LocalizedString("Firmware Version", comment: "The title of the cell showing the pump firmware version")
  137. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpFirmwareVersion)
  138. return cell
  139. case .pumpRegion:
  140. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  141. cell.textLabel?.text = LocalizedString("Region", comment: "The title of the cell showing the pump region")
  142. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpRegion)
  143. return cell
  144. }
  145. case .actions:
  146. switch ActionsRow(rawValue: indexPath.row)! {
  147. case .suspendResume:
  148. let cell = tableView.dequeueReusableCell(withIdentifier: SuspendResumeTableViewCell.className, for: indexPath) as! SuspendResumeTableViewCell
  149. cell.basalDeliveryState = pumpManager.status.basalDeliveryState
  150. return cell
  151. }
  152. case .settings:
  153. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  154. switch SettingsRow(rawValue: indexPath.row)! {
  155. case .batteryChemistry:
  156. cell.textLabel?.text = LocalizedString("Pump Battery Type", comment: "The title text for the battery type value")
  157. cell.detailTextLabel?.text = String(describing: pumpManager.batteryChemistry)
  158. case .preferredInsulinDataSource:
  159. cell.textLabel?.text = LocalizedString("Preferred Data Source", comment: "The title text for the preferred insulin data source config")
  160. cell.detailTextLabel?.text = String(describing: pumpManager.preferredInsulinDataSource)
  161. case .timeZoneOffset:
  162. cell.textLabel?.text = LocalizedString("Change Time Zone", comment: "The title of the command to change pump time zone")
  163. let localTimeZone = TimeZone.current
  164. let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier
  165. let timeZoneDiff = TimeInterval(pumpManager.state.timeZone.secondsFromGMT() - localTimeZone.secondsFromGMT())
  166. let formatter = DateComponentsFormatter()
  167. formatter.allowedUnits = [.hour, .minute]
  168. let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : ""
  169. cell.detailTextLabel?.text = String(format: LocalizedString("%1$@%2$@%3$@", comment: "The format string for displaying an offset from a time zone: (1: GMT)(2: -)(3: 4:00)"), localTimeZoneName, timeZoneDiff != 0 ? (timeZoneDiff < 0 ? "-" : "+") : "", diffString)
  170. case .insulinType:
  171. cell.prepareForReuse()
  172. cell.textLabel?.text = "Insulin Type"
  173. cell.detailTextLabel?.text = pumpManager.insulinType?.brandName
  174. }
  175. cell.accessoryType = .disclosureIndicator
  176. return cell
  177. case .rileyLinks:
  178. return super.tableView(tableView, cellForRowAt: indexPath)
  179. case .delete:
  180. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  181. cell.textLabel?.text = LocalizedString("Delete Pump", comment: "Title text for the button to remove a pump from Loop")
  182. cell.textLabel?.textAlignment = .center
  183. cell.tintColor = .deleteColor
  184. cell.isEnabled = true
  185. return cell
  186. }
  187. }
  188. override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  189. switch Section(rawValue: indexPath.section)! {
  190. case .info:
  191. return false
  192. case .actions, .settings, .rileyLinks, .delete:
  193. return true
  194. }
  195. }
  196. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  197. let sender = tableView.cellForRow(at: indexPath)
  198. switch Section(rawValue: indexPath.section)! {
  199. case .info:
  200. break
  201. case .actions:
  202. switch ActionsRow(rawValue: indexPath.row)! {
  203. case .suspendResume:
  204. suspendResumeCellTapped(sender as! SuspendResumeTableViewCell)
  205. tableView.deselectRow(at: indexPath, animated: true)
  206. }
  207. case .settings:
  208. switch SettingsRow(rawValue: indexPath.row)! {
  209. case .timeZoneOffset:
  210. let vc = CommandResponseViewController.changeTime(ops: pumpManager.pumpOps, rileyLinkDeviceProvider: pumpManager.rileyLinkDeviceProvider)
  211. vc.title = sender?.textLabel?.text
  212. show(vc, sender: indexPath)
  213. case .batteryChemistry:
  214. let vc = RadioSelectionTableViewController.batteryChemistryType(pumpManager.batteryChemistry)
  215. vc.title = sender?.textLabel?.text
  216. vc.delegate = self
  217. show(vc, sender: sender)
  218. case .preferredInsulinDataSource:
  219. let vc = RadioSelectionTableViewController.insulinDataSource(pumpManager.preferredInsulinDataSource)
  220. vc.title = sender?.textLabel?.text
  221. vc.delegate = self
  222. show(vc, sender: sender)
  223. case .insulinType:
  224. let view = InsulinTypeSetting(initialValue: pumpManager.insulinType ?? .novolog, supportedInsulinTypes: InsulinType.allCases) { (newType) in
  225. self.pumpManager.insulinType = newType
  226. }
  227. let vc = DismissibleHostingController(rootView: view)
  228. vc.title = LocalizedString("Insulin Type", comment: "Controller title for insulin type selection screen")
  229. show(vc, sender: sender)
  230. }
  231. case .rileyLinks:
  232. let device = devicesDataSource.devices[indexPath.row]
  233. let vc = RileyLinkMinimedDeviceTableViewController(
  234. device: device,
  235. pumpOps: pumpManager.pumpOps
  236. )
  237. self.show(vc, sender: sender)
  238. case .delete:
  239. let confirmVC = UIAlertController(pumpDeletionHandler: {
  240. self.pumpManager.notifyDelegateOfDeactivation {
  241. DispatchQueue.main.async {
  242. self.done()
  243. }
  244. }
  245. })
  246. present(confirmVC, animated: true) {
  247. tableView.deselectRow(at: indexPath, animated: true)
  248. }
  249. }
  250. }
  251. override func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
  252. switch Section(rawValue: indexPath.section)! {
  253. case .settings:
  254. switch SettingsRow(rawValue: indexPath.row)! {
  255. case .timeZoneOffset, .insulinType:
  256. tableView.reloadRows(at: [indexPath], with: .fade)
  257. case .batteryChemistry:
  258. break
  259. case .preferredInsulinDataSource:
  260. break
  261. }
  262. case .info, .actions, .rileyLinks, .delete:
  263. break
  264. }
  265. return indexPath
  266. }
  267. }
  268. extension MinimedPumpSettingsViewController: RadioSelectionTableViewControllerDelegate {
  269. func radioSelectionTableViewControllerDidChangeSelectedIndex(_ controller: RadioSelectionTableViewController) {
  270. guard let indexPath = self.tableView.indexPathForSelectedRow else {
  271. return
  272. }
  273. switch Section(rawValue: indexPath.section)! {
  274. case .settings:
  275. switch SettingsRow(rawValue: indexPath.row)! {
  276. case .preferredInsulinDataSource:
  277. if let selectedIndex = controller.selectedIndex, let dataSource = InsulinDataSource(rawValue: selectedIndex) {
  278. pumpManager.preferredInsulinDataSource = dataSource
  279. }
  280. case .batteryChemistry:
  281. if let selectedIndex = controller.selectedIndex, let dataSource = MinimedKit.BatteryChemistryType(rawValue: selectedIndex) {
  282. pumpManager.batteryChemistry = dataSource
  283. }
  284. default:
  285. assertionFailure()
  286. }
  287. default:
  288. assertionFailure()
  289. }
  290. tableView.reloadRows(at: [indexPath], with: .none)
  291. }
  292. private func suspendResumeCellTapped(_ cell: SuspendResumeTableViewCell) {
  293. guard cell.isEnabled else {
  294. return
  295. }
  296. switch cell.shownAction {
  297. case .resume:
  298. pumpManager.resumeDelivery { (error) in
  299. if let error = error {
  300. DispatchQueue.main.async {
  301. let title = LocalizedString("Error Resuming", comment: "The alert title for a resume error")
  302. self.present(UIAlertController(with: error, title: title), animated: true)
  303. }
  304. }
  305. }
  306. case .suspend:
  307. pumpManager.suspendDelivery { (error) in
  308. if let error = error {
  309. DispatchQueue.main.async {
  310. let title = LocalizedString("Error Suspending", comment: "The alert title for a suspend error")
  311. self.present(UIAlertController(with: error, title: title), animated: true)
  312. }
  313. }
  314. }
  315. default:
  316. break
  317. }
  318. }
  319. }
  320. extension MinimedPumpSettingsViewController: PumpManagerStatusObserver {
  321. public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  322. dispatchPrecondition(condition: .onQueue(.main))
  323. let suspendResumeTableViewCell = self.tableView?.cellForRow(at: IndexPath(row: ActionsRow.suspendResume.rawValue, section: Section.actions.rawValue)) as! SuspendResumeTableViewCell
  324. suspendResumeTableViewCell.basalDeliveryState = status.basalDeliveryState
  325. }
  326. }
  327. private extension UIAlertController {
  328. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  329. self.init(
  330. title: nil,
  331. message: LocalizedString("Are you sure you want to delete this pump?", comment: "Confirmation message for deleting a pump"),
  332. preferredStyle: .actionSheet
  333. )
  334. addAction(UIAlertAction(
  335. title: LocalizedString("Delete Pump", comment: "Button title to delete pump"),
  336. style: .destructive,
  337. handler: { (_) in
  338. handler()
  339. }
  340. ))
  341. let cancel = LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet")
  342. addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
  343. }
  344. }