MinimedPumpSettingsViewController.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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. // This should always be last so it can be omitted for non-MySentry pumps:
  82. case useMySentry
  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 .info:
  91. return InfoRow.allCases.count
  92. case .actions:
  93. return ActionsRow.allCases.count
  94. case .settings:
  95. let settingsRowCount = pumpManager.state.pumpModel.hasMySentry ? SettingsRow.allCases.count : SettingsRow.allCases.count - 1
  96. return settingsRowCount
  97. case .rileyLinks:
  98. return super.tableView(tableView, numberOfRowsInSection: section)
  99. case .delete:
  100. return 1
  101. }
  102. }
  103. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  104. switch Section(rawValue: section)! {
  105. case .settings:
  106. return LocalizedString("Configuration", comment: "The title of the configuration section in settings")
  107. case .rileyLinks:
  108. return super.tableView(tableView, titleForHeaderInSection: section)
  109. case .delete:
  110. return " " // Use an empty string for more dramatic spacing
  111. case .info, .actions:
  112. return nil
  113. }
  114. }
  115. override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  116. switch Section(rawValue: section)! {
  117. case .rileyLinks:
  118. return super.tableView(tableView, viewForHeaderInSection: section)
  119. case .info, .settings, .delete, .actions:
  120. return nil
  121. }
  122. }
  123. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  124. switch Section(rawValue: indexPath.section)! {
  125. case .info:
  126. switch InfoRow(rawValue: indexPath.row)! {
  127. case .pumpID:
  128. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  129. cell.textLabel?.text = LocalizedString("Pump ID", comment: "The title text for the pump ID config value")
  130. cell.detailTextLabel?.text = pumpManager.state.pumpID
  131. return cell
  132. case .pumpModel:
  133. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  134. cell.textLabel?.text = LocalizedString("Pump Model", comment: "The title of the cell showing the pump model number")
  135. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpModel)
  136. return cell
  137. case .pumpFirmware:
  138. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  139. cell.textLabel?.text = LocalizedString("Firmware Version", comment: "The title of the cell showing the pump firmware version")
  140. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpFirmwareVersion)
  141. return cell
  142. case .pumpRegion:
  143. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  144. cell.textLabel?.text = LocalizedString("Region", comment: "The title of the cell showing the pump region")
  145. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpRegion)
  146. return cell
  147. }
  148. case .actions:
  149. switch ActionsRow(rawValue: indexPath.row)! {
  150. case .suspendResume:
  151. let cell = tableView.dequeueReusableCell(withIdentifier: SuspendResumeTableViewCell.className, for: indexPath) as! SuspendResumeTableViewCell
  152. cell.basalDeliveryState = pumpManager.status.basalDeliveryState
  153. return cell
  154. }
  155. case .settings:
  156. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  157. switch SettingsRow(rawValue: indexPath.row)! {
  158. case .batteryChemistry:
  159. cell.textLabel?.text = LocalizedString("Pump Battery Type", comment: "The title text for the battery type value")
  160. cell.detailTextLabel?.text = String(describing: pumpManager.batteryChemistry)
  161. case .preferredInsulinDataSource:
  162. cell.textLabel?.text = LocalizedString("Preferred Data Source", comment: "The title text for the preferred insulin data source config")
  163. cell.detailTextLabel?.text = String(describing: pumpManager.preferredInsulinDataSource)
  164. case .useMySentry:
  165. cell.textLabel?.text = LocalizedString("Use MySentry", comment: "The title text for the preferred MySentry setting config")
  166. cell.detailTextLabel?.text = pumpManager.useMySentry ? "Yes" : "No"
  167. case .timeZoneOffset:
  168. cell.textLabel?.text = LocalizedString("Change Time Zone", comment: "The title of the command to change pump time zone")
  169. let localTimeZone = TimeZone.current
  170. let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier
  171. let timeZoneDiff = TimeInterval(pumpManager.state.timeZone.secondsFromGMT() - localTimeZone.secondsFromGMT())
  172. let formatter = DateComponentsFormatter()
  173. formatter.allowedUnits = [.hour, .minute]
  174. let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : ""
  175. 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)
  176. case .insulinType:
  177. cell.prepareForReuse()
  178. cell.textLabel?.text = "Insulin Type"
  179. cell.detailTextLabel?.text = pumpManager.insulinType?.brandName
  180. }
  181. cell.accessoryType = .disclosureIndicator
  182. return cell
  183. case .rileyLinks:
  184. return super.tableView(tableView, cellForRowAt: indexPath)
  185. case .delete:
  186. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  187. cell.textLabel?.text = LocalizedString("Delete Pump", comment: "Title text for the button to remove a pump from Loop")
  188. cell.textLabel?.textAlignment = .center
  189. cell.tintColor = .deleteColor
  190. cell.isEnabled = true
  191. return cell
  192. }
  193. }
  194. override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  195. switch Section(rawValue: indexPath.section)! {
  196. case .info:
  197. return false
  198. case .actions, .settings, .rileyLinks, .delete:
  199. return true
  200. }
  201. }
  202. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  203. let sender = tableView.cellForRow(at: indexPath)
  204. switch Section(rawValue: indexPath.section)! {
  205. case .info:
  206. break
  207. case .actions:
  208. switch ActionsRow(rawValue: indexPath.row)! {
  209. case .suspendResume:
  210. suspendResumeCellTapped(sender as! SuspendResumeTableViewCell)
  211. tableView.deselectRow(at: indexPath, animated: true)
  212. }
  213. case .settings:
  214. switch SettingsRow(rawValue: indexPath.row)! {
  215. case .timeZoneOffset:
  216. let vc = CommandResponseViewController.changeTime(ops: pumpManager.pumpOps, rileyLinkDeviceProvider: pumpManager.rileyLinkDeviceProvider)
  217. vc.title = sender?.textLabel?.text
  218. show(vc, sender: indexPath)
  219. case .batteryChemistry:
  220. let vc = RadioSelectionTableViewController.batteryChemistryType(pumpManager.batteryChemistry)
  221. vc.title = sender?.textLabel?.text
  222. vc.delegate = self
  223. show(vc, sender: sender)
  224. case .preferredInsulinDataSource:
  225. let vc = RadioSelectionTableViewController.insulinDataSource(pumpManager.preferredInsulinDataSource)
  226. vc.title = sender?.textLabel?.text
  227. vc.delegate = self
  228. show(vc, sender: sender)
  229. case .insulinType:
  230. let view = InsulinTypeSetting(initialValue: pumpManager.insulinType ?? .novolog, supportedInsulinTypes: InsulinType.allCases) { (newType) in
  231. self.pumpManager.insulinType = newType
  232. }
  233. let vc = DismissibleHostingController(rootView: view)
  234. vc.title = LocalizedString("Insulin Type", comment: "Controller title for insulin type selection screen")
  235. show(vc, sender: sender)
  236. case .useMySentry:
  237. let vc = RadioSelectionTableViewController.useMySentry(pumpManager.useMySentry)
  238. vc.title = sender?.textLabel?.text
  239. vc.delegate = self
  240. show(vc, sender: sender)
  241. }
  242. case .rileyLinks:
  243. let device = devicesDataSource.devices[indexPath.row]
  244. guard device.hardwareType != nil else {
  245. tableView.deselectRow(at: indexPath, animated: true)
  246. return
  247. }
  248. let vc = RileyLinkDeviceTableViewController(
  249. device: device,
  250. batteryAlertLevel: pumpManager.rileyLinkBatteryAlertLevel,
  251. batteryAlertLevelChanged: { [weak self] value in
  252. self?.pumpManager.rileyLinkBatteryAlertLevel = value
  253. }
  254. )
  255. self.show(vc, sender: sender)
  256. case .delete:
  257. let confirmVC = UIAlertController(pumpDeletionHandler: {
  258. self.pumpManager.notifyDelegateOfDeactivation {
  259. DispatchQueue.main.async {
  260. self.done()
  261. }
  262. }
  263. })
  264. present(confirmVC, animated: true) {
  265. tableView.deselectRow(at: indexPath, animated: true)
  266. }
  267. }
  268. }
  269. override func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
  270. switch Section(rawValue: indexPath.section)! {
  271. case .settings:
  272. switch SettingsRow(rawValue: indexPath.row)! {
  273. case .timeZoneOffset, .insulinType:
  274. tableView.reloadRows(at: [indexPath], with: .fade)
  275. case .batteryChemistry:
  276. break
  277. case .preferredInsulinDataSource:
  278. break
  279. case .useMySentry:
  280. break
  281. }
  282. case .info, .actions, .rileyLinks, .delete:
  283. break
  284. }
  285. return indexPath
  286. }
  287. }
  288. extension MinimedPumpSettingsViewController: RadioSelectionTableViewControllerDelegate {
  289. func radioSelectionTableViewControllerDidChangeSelectedIndex(_ controller: RadioSelectionTableViewController) {
  290. guard let indexPath = self.tableView.indexPathForSelectedRow else {
  291. return
  292. }
  293. switch Section(rawValue: indexPath.section)! {
  294. case .settings:
  295. switch SettingsRow(rawValue: indexPath.row)! {
  296. case .preferredInsulinDataSource:
  297. if let selectedIndex = controller.selectedIndex, let dataSource = InsulinDataSource(rawValue: selectedIndex) {
  298. pumpManager.preferredInsulinDataSource = dataSource
  299. }
  300. case .batteryChemistry:
  301. if let selectedIndex = controller.selectedIndex, let dataSource = MinimedKit.BatteryChemistryType(rawValue: selectedIndex) {
  302. pumpManager.batteryChemistry = dataSource
  303. }
  304. case .useMySentry:
  305. if let selectedIndex = controller.selectedIndex {
  306. pumpManager.useMySentry = selectedIndex == 0
  307. }
  308. default:
  309. assertionFailure()
  310. }
  311. default:
  312. assertionFailure()
  313. }
  314. tableView.reloadRows(at: [indexPath], with: .none)
  315. }
  316. private func suspendResumeCellTapped(_ cell: SuspendResumeTableViewCell) {
  317. guard cell.isEnabled else {
  318. return
  319. }
  320. switch cell.shownAction {
  321. case .resume:
  322. pumpManager.resumeDelivery { (error) in
  323. if let error = error {
  324. DispatchQueue.main.async {
  325. let title = LocalizedString("Error Resuming", comment: "The alert title for a resume error")
  326. self.present(UIAlertController(with: error, title: title), animated: true)
  327. }
  328. }
  329. }
  330. case .suspend:
  331. pumpManager.suspendDelivery { (error) in
  332. if let error = error {
  333. DispatchQueue.main.async {
  334. let title = LocalizedString("Error Suspending", comment: "The alert title for a suspend error")
  335. self.present(UIAlertController(with: error, title: title), animated: true)
  336. }
  337. }
  338. }
  339. default:
  340. break
  341. }
  342. }
  343. }
  344. extension MinimedPumpSettingsViewController: PumpManagerStatusObserver {
  345. public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  346. dispatchPrecondition(condition: .onQueue(.main))
  347. let suspendResumeTableViewCell = self.tableView?.cellForRow(at: IndexPath(row: ActionsRow.suspendResume.rawValue, section: Section.actions.rawValue)) as! SuspendResumeTableViewCell
  348. suspendResumeTableViewCell.basalDeliveryState = status.basalDeliveryState
  349. }
  350. }
  351. private extension UIAlertController {
  352. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  353. self.init(
  354. title: nil,
  355. message: LocalizedString("Are you sure you want to delete this pump?", comment: "Confirmation message for deleting a pump"),
  356. preferredStyle: .actionSheet
  357. )
  358. addAction(UIAlertAction(
  359. title: LocalizedString("Delete Pump", comment: "Button title to delete pump"),
  360. style: .destructive,
  361. handler: { (_) in
  362. handler()
  363. }
  364. ))
  365. let cancel = LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet")
  366. addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
  367. }
  368. }