MinimedPumpSettingsViewController.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  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 RileyLinkBLEKit
  11. import RileyLinkKitUI
  12. import LoopKit
  13. class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
  14. let pumpManager: MinimedPumpManager
  15. let supportedInsulinTypes: [InsulinType]
  16. private var ops: PumpOps {
  17. return pumpManager.pumpOps
  18. }
  19. // MARK: - Formatters
  20. private lazy var dateFormatter: DateFormatter = {
  21. let dateFormatter = DateFormatter()
  22. dateFormatter.dateStyle = .none
  23. dateFormatter.timeStyle = .medium
  24. return dateFormatter
  25. }()
  26. private lazy var measurementFormatter: MeasurementFormatter = {
  27. let formatter = MeasurementFormatter()
  28. formatter.numberFormatter = decimalFormatter
  29. return formatter
  30. }()
  31. private lazy var decimalFormatter: NumberFormatter = {
  32. let decimalFormatter = NumberFormatter()
  33. decimalFormatter.numberStyle = .decimal
  34. decimalFormatter.minimumSignificantDigits = 5
  35. return decimalFormatter
  36. }()
  37. private lazy var integerFormatter = NumberFormatter()
  38. private func cellForRow(_ row: CommandsRow) -> UITableViewCell? {
  39. return tableView.cellForRow(at: IndexPath(row: row.rawValue, section: Section.commands.rawValue))
  40. }
  41. private var pumpState: PumpState? {
  42. didSet {
  43. if let cell = cellForRow(.tune) {
  44. cell.setTuneInfo(lastValidFrequency: pumpState?.lastValidFrequency, lastTuned: pumpState?.lastTuned, measurementFormatter: measurementFormatter, dateFormatter: dateFormatter)
  45. }
  46. }
  47. }
  48. init(pumpManager: MinimedPumpManager, supportedInsulinTypes: [InsulinType]) {
  49. self.pumpManager = pumpManager
  50. self.supportedInsulinTypes = supportedInsulinTypes
  51. super.init(rileyLinkPumpManager: pumpManager, devicesSectionIndex: Section.rileyLinks.rawValue, style: .grouped)
  52. }
  53. required init?(coder aDecoder: NSCoder) {
  54. fatalError("init(coder:) has not been implemented")
  55. }
  56. override func viewDidLoad() {
  57. super.viewDidLoad()
  58. title = LocalizedString("Pump Settings", comment: "Title of the pump settings view controller")
  59. tableView.rowHeight = UITableView.automaticDimension
  60. tableView.estimatedRowHeight = 44
  61. tableView.sectionHeaderHeight = UITableView.automaticDimension
  62. tableView.estimatedSectionHeaderHeight = 55
  63. tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className)
  64. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  65. tableView.register(SuspendResumeTableViewCell.self, forCellReuseIdentifier: SuspendResumeTableViewCell.className)
  66. let imageView = UIImageView(image: pumpManager.state.largePumpImage)
  67. imageView.contentMode = .bottom
  68. imageView.frame.size.height += 18 // feels right
  69. tableView.tableHeaderView = imageView
  70. let center = NotificationCenter.default
  71. let mainQueue = OperationQueue.main
  72. center.addObserver(forName: .PumpOpsStateDidChange, object: pumpManager.pumpOps, queue: mainQueue) { [weak self] (note) in
  73. if let state = note.userInfo?[MinimedPumpOps.notificationPumpStateKey] as? PumpState {
  74. self?.pumpState = state
  75. }
  76. }
  77. pumpManager.addStatusObserver(self, queue: .main)
  78. let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
  79. self.navigationItem.setRightBarButton(button, animated: false)
  80. self.pumpState = pumpManager.state.pumpState
  81. }
  82. @objc func doneTapped(_ sender: Any) {
  83. done()
  84. }
  85. private func done() {
  86. if let nav = navigationController as? SettingsNavigationViewController {
  87. nav.notifyComplete()
  88. }
  89. if let nav = navigationController as? MinimedPumpManagerSetupViewController {
  90. nav.finishedSettingsDisplay()
  91. }
  92. }
  93. override func viewWillAppear(_ animated: Bool) {
  94. if clearsSelectionOnViewWillAppear {
  95. // Manually invoke the delegate for rows deselecting on appear
  96. for indexPath in tableView.indexPathsForSelectedRows ?? [] {
  97. _ = tableView(tableView, willDeselectRowAt: indexPath)
  98. }
  99. }
  100. super.viewWillAppear(animated)
  101. }
  102. // MARK: - Data Source
  103. private enum Section: Int, CaseIterable {
  104. case info = 0
  105. case actions
  106. case settings
  107. case rileyLinks
  108. case commands
  109. case delete
  110. }
  111. private enum InfoRow: Int, CaseIterable {
  112. case pumpID = 0
  113. case pumpModel
  114. case pumpFirmware
  115. case pumpRegion
  116. }
  117. private enum ActionsRow: Int, CaseIterable {
  118. case suspendResume = 0
  119. }
  120. private enum SettingsRow: Int, CaseIterable {
  121. case timeZoneOffset = 0
  122. case batteryChemistry
  123. case preferredInsulinDataSource
  124. case insulinType
  125. // This should always be last so it can be omitted for non-MySentry pumps:
  126. case useMySentry
  127. }
  128. private enum CommandsRow: Int, CaseIterable {
  129. case tune
  130. case mySentryPair
  131. case dumpHistory
  132. case fetchGlucose
  133. case getPumpModel
  134. case pressDownButton
  135. case readPumpStatus
  136. case readBasalSchedule
  137. }
  138. // MARK: UITableViewDataSource
  139. override func numberOfSections(in tableView: UITableView) -> Int {
  140. return Section.allCases.count
  141. }
  142. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  143. switch Section(rawValue: section)! {
  144. case .info:
  145. return InfoRow.allCases.count
  146. case .actions:
  147. return ActionsRow.allCases.count
  148. case .settings:
  149. let settingsRowCount = pumpManager.state.pumpModel.hasMySentry ? SettingsRow.allCases.count : SettingsRow.allCases.count - 1
  150. return settingsRowCount
  151. case .rileyLinks:
  152. return super.tableView(tableView, numberOfRowsInSection: section)
  153. case .commands:
  154. return CommandsRow.allCases.count
  155. case .delete:
  156. return 1
  157. }
  158. }
  159. override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  160. switch Section(rawValue: section)! {
  161. case .settings:
  162. return LocalizedString("Configuration", comment: "The title of the configuration section in MinimedPumpManager settings")
  163. case .rileyLinks:
  164. return super.tableView(tableView, titleForHeaderInSection: section)
  165. case .commands:
  166. return LocalizedString("Commands", comment: "The title of the commands section in MinimedPumpManager settings")
  167. case .delete:
  168. return " " // Use an empty string for more dramatic spacing
  169. case .info, .actions:
  170. return nil
  171. }
  172. }
  173. override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  174. switch Section(rawValue: section)! {
  175. case .rileyLinks:
  176. return super.tableView(tableView, viewForHeaderInSection: section)
  177. case .info, .settings, .delete, .actions, .commands:
  178. return nil
  179. }
  180. }
  181. override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  182. switch Section(rawValue: indexPath.section)! {
  183. case .info:
  184. switch InfoRow(rawValue: indexPath.row)! {
  185. case .pumpID:
  186. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  187. cell.textLabel?.text = LocalizedString("Pump ID", comment: "The title text for the pump ID config value")
  188. cell.detailTextLabel?.text = pumpManager.state.pumpID
  189. return cell
  190. case .pumpModel:
  191. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  192. cell.textLabel?.text = LocalizedString("Pump Model", comment: "The title of the cell showing the pump model number")
  193. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpModel)
  194. return cell
  195. case .pumpFirmware:
  196. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  197. cell.textLabel?.text = LocalizedString("Firmware Version", comment: "The title of the cell showing the pump firmware version")
  198. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpFirmwareVersion)
  199. return cell
  200. case .pumpRegion:
  201. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  202. cell.textLabel?.text = LocalizedString("Region", comment: "The title of the cell showing the pump region")
  203. cell.detailTextLabel?.text = String(describing: pumpManager.state.pumpRegion)
  204. return cell
  205. }
  206. case .actions:
  207. switch ActionsRow(rawValue: indexPath.row)! {
  208. case .suspendResume:
  209. let cell = tableView.dequeueReusableCell(withIdentifier: SuspendResumeTableViewCell.className, for: indexPath) as! SuspendResumeTableViewCell
  210. cell.basalDeliveryState = pumpManager.status.basalDeliveryState
  211. return cell
  212. }
  213. case .settings:
  214. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  215. switch SettingsRow(rawValue: indexPath.row)! {
  216. case .batteryChemistry:
  217. cell.textLabel?.text = LocalizedString("Pump Battery Type", comment: "The title text for the battery type value")
  218. cell.detailTextLabel?.text = String(describing: pumpManager.batteryChemistry)
  219. case .preferredInsulinDataSource:
  220. cell.textLabel?.text = LocalizedString("Preferred Data Source", comment: "The title text for the preferred insulin data source config")
  221. cell.detailTextLabel?.text = String(describing: pumpManager.preferredInsulinDataSource)
  222. case .useMySentry:
  223. cell.textLabel?.text = LocalizedString("Use MySentry", comment: "The title text for the preferred MySentry setting config")
  224. cell.detailTextLabel?.text = pumpManager.useMySentry ? "Yes" : "No"
  225. case .timeZoneOffset:
  226. cell.textLabel?.text = LocalizedString("Change Time Zone", comment: "The title of the command to change pump time zone")
  227. let localTimeZone = TimeZone.current
  228. let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier
  229. let timeZoneDiff = TimeInterval(pumpManager.state.timeZone.secondsFromGMT() - localTimeZone.secondsFromGMT())
  230. let formatter = DateComponentsFormatter()
  231. formatter.allowedUnits = [.hour, .minute]
  232. let diffString = timeZoneDiff != 0 ? formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff)) : ""
  233. 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)
  234. case .insulinType:
  235. cell.prepareForReuse()
  236. cell.textLabel?.text = "Insulin Type"
  237. cell.detailTextLabel?.text = pumpManager.insulinType?.brandName
  238. }
  239. cell.accessoryType = .disclosureIndicator
  240. return cell
  241. case .rileyLinks:
  242. return super.tableView(tableView, cellForRowAt: indexPath)
  243. case .commands:
  244. let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
  245. switch CommandsRow(rawValue: indexPath.row)! {
  246. case .tune:
  247. cell.setTuneInfo(lastValidFrequency: pumpState?.lastValidFrequency, lastTuned: pumpState?.lastTuned, measurementFormatter: measurementFormatter, dateFormatter: dateFormatter)
  248. case .mySentryPair:
  249. cell.textLabel?.text = LocalizedString("MySentry Pair", comment: "The title of the command to pair with mysentry")
  250. case .dumpHistory:
  251. cell.textLabel?.text = LocalizedString("Fetch Recent History", comment: "The title of the command to fetch recent history")
  252. case .fetchGlucose:
  253. cell.textLabel?.text = LocalizedString("Fetch Enlite Glucose", comment: "The title of the command to fetch recent glucose")
  254. case .getPumpModel:
  255. cell.textLabel?.text = LocalizedString("Get Pump Model", comment: "The title of the command to get pump model")
  256. case .pressDownButton:
  257. cell.textLabel?.text = LocalizedString("Send Button Press", comment: "The title of the command to send a button press")
  258. case .readPumpStatus:
  259. cell.textLabel?.text = LocalizedString("Read Pump Status", comment: "The title of the command to read pump status")
  260. case .readBasalSchedule:
  261. cell.textLabel?.text = LocalizedString("Read Basal Schedule", comment: "The title of the command to read basal schedule")
  262. }
  263. return cell
  264. case .delete:
  265. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  266. cell.textLabel?.text = LocalizedString("Delete Pump", comment: "Title text for the button to remove a pump from Loop")
  267. cell.textLabel?.textAlignment = .center
  268. cell.tintColor = .deleteColor
  269. cell.isEnabled = true
  270. return cell
  271. }
  272. }
  273. override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  274. switch Section(rawValue: indexPath.section)! {
  275. case .info:
  276. return false
  277. case .actions, .settings, .rileyLinks, .delete, .commands:
  278. return true
  279. }
  280. }
  281. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  282. let sender = tableView.cellForRow(at: indexPath)
  283. switch Section(rawValue: indexPath.section)! {
  284. case .info:
  285. break
  286. case .actions:
  287. switch ActionsRow(rawValue: indexPath.row)! {
  288. case .suspendResume:
  289. suspendResumeCellTapped(sender as! SuspendResumeTableViewCell)
  290. tableView.deselectRow(at: indexPath, animated: true)
  291. }
  292. case .settings:
  293. switch SettingsRow(rawValue: indexPath.row)! {
  294. case .timeZoneOffset:
  295. let vc = CommandResponseViewController.changeTime(ops: pumpManager.pumpOps, rileyLinkDeviceProvider: pumpManager.rileyLinkDeviceProvider)
  296. vc.title = sender?.textLabel?.text
  297. show(vc, sender: indexPath)
  298. case .batteryChemistry:
  299. let vc = RadioSelectionTableViewController.batteryChemistryType(pumpManager.batteryChemistry)
  300. vc.title = sender?.textLabel?.text
  301. vc.delegate = self
  302. show(vc, sender: sender)
  303. case .preferredInsulinDataSource:
  304. let vc = RadioSelectionTableViewController.insulinDataSource(pumpManager.preferredInsulinDataSource)
  305. vc.title = sender?.textLabel?.text
  306. vc.delegate = self
  307. show(vc, sender: sender)
  308. case .insulinType:
  309. let view = InsulinTypeSetting(initialValue: pumpManager.insulinType ?? .novolog, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false) { (newType) in
  310. self.pumpManager.insulinType = newType
  311. }
  312. let vc = DismissibleHostingController(rootView: view)
  313. vc.title = LocalizedString("Insulin Type", comment: "Controller title for insulin type selection screen")
  314. show(vc, sender: sender)
  315. case .useMySentry:
  316. let vc = RadioSelectionTableViewController.useMySentry(pumpManager.useMySentry)
  317. vc.title = sender?.textLabel?.text
  318. vc.delegate = self
  319. show(vc, sender: sender)
  320. }
  321. case .rileyLinks:
  322. let device = devicesDataSource.devices[indexPath.row]
  323. guard device.hardwareType != nil else {
  324. tableView.deselectRow(at: indexPath, animated: true)
  325. return
  326. }
  327. let vc = RileyLinkDeviceTableViewController(
  328. device: device,
  329. batteryAlertLevel: pumpManager.rileyLinkBatteryAlertLevel,
  330. batteryAlertLevelChanged: { [weak self] value in
  331. self?.pumpManager.rileyLinkBatteryAlertLevel = value
  332. }
  333. )
  334. self.show(vc, sender: sender)
  335. case .commands:
  336. pumpManager.rileyLinkDeviceProvider.firstConnectedDevice { device in
  337. DispatchQueue.main.async {
  338. if let device = device,
  339. let cell = tableView.cellForRow(at: indexPath),
  340. let title = cell.textLabel?.text
  341. {
  342. self.runCommand(CommandsRow(rawValue: indexPath.row)!, device: device, title: title)
  343. }
  344. }
  345. }
  346. case .delete:
  347. let confirmVC = UIAlertController(pumpDeletionHandler: {
  348. self.pumpManager.notifyDelegateOfDeactivation {
  349. DispatchQueue.main.async {
  350. self.done()
  351. }
  352. }
  353. })
  354. present(confirmVC, animated: true) {
  355. tableView.deselectRow(at: indexPath, animated: true)
  356. }
  357. }
  358. }
  359. override func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
  360. switch Section(rawValue: indexPath.section)! {
  361. case .settings:
  362. switch SettingsRow(rawValue: indexPath.row)! {
  363. case .timeZoneOffset, .insulinType:
  364. tableView.reloadRows(at: [indexPath], with: .fade)
  365. case .batteryChemistry:
  366. break
  367. case .preferredInsulinDataSource:
  368. break
  369. case .useMySentry:
  370. break
  371. }
  372. case .info, .actions, .rileyLinks, .delete, .commands:
  373. break
  374. }
  375. return indexPath
  376. }
  377. private func runCommand(_ command: CommandsRow, device: RileyLinkDevice, title: String) {
  378. var vc: CommandResponseViewController?
  379. switch command {
  380. case .tune:
  381. vc = .tuneRadio(ops: ops, device: device, measurementFormatter: measurementFormatter)
  382. case .mySentryPair:
  383. vc = .mySentryPair(ops: ops, device: device)
  384. case .dumpHistory:
  385. vc = .dumpHistory(ops: ops, device: device)
  386. case .fetchGlucose:
  387. vc = .fetchGlucose(ops: ops, device: device)
  388. case .getPumpModel:
  389. vc = .getPumpModel(ops: ops, device: device)
  390. case .pressDownButton:
  391. vc = .pressDownButton(ops: ops, device: device)
  392. case .readPumpStatus:
  393. vc = .readPumpStatus(ops: ops, device: device, measurementFormatter: measurementFormatter)
  394. case .readBasalSchedule:
  395. vc = .readBasalSchedule(ops: ops, device: device, integerFormatter: integerFormatter)
  396. }
  397. vc?.title = title
  398. if let vc = vc {
  399. show(vc, sender: nil)
  400. }
  401. }
  402. }
  403. extension MinimedPumpSettingsViewController: RadioSelectionTableViewControllerDelegate {
  404. func radioSelectionTableViewControllerDidChangeSelectedIndex(_ controller: RadioSelectionTableViewController) {
  405. guard let indexPath = self.tableView.indexPathForSelectedRow else {
  406. return
  407. }
  408. switch Section(rawValue: indexPath.section)! {
  409. case .settings:
  410. switch SettingsRow(rawValue: indexPath.row)! {
  411. case .preferredInsulinDataSource:
  412. if let selectedIndex = controller.selectedIndex, let dataSource = InsulinDataSource(rawValue: selectedIndex) {
  413. pumpManager.preferredInsulinDataSource = dataSource
  414. }
  415. case .batteryChemistry:
  416. if let selectedIndex = controller.selectedIndex, let dataSource = MinimedKit.BatteryChemistryType(rawValue: selectedIndex) {
  417. pumpManager.batteryChemistry = dataSource
  418. }
  419. case .useMySentry:
  420. if let selectedIndex = controller.selectedIndex {
  421. pumpManager.useMySentry = selectedIndex == 0
  422. }
  423. default:
  424. assertionFailure()
  425. }
  426. default:
  427. assertionFailure()
  428. }
  429. tableView.reloadRows(at: [indexPath], with: .none)
  430. }
  431. private func suspendResumeCellTapped(_ cell: SuspendResumeTableViewCell) {
  432. guard cell.isEnabled else {
  433. return
  434. }
  435. switch cell.shownAction {
  436. case .resume:
  437. pumpManager.resumeDelivery { (error) in
  438. if let error = error {
  439. DispatchQueue.main.async {
  440. let title = LocalizedString("Error Resuming", comment: "The alert title for a resume error")
  441. self.present(UIAlertController(with: error, title: title), animated: true)
  442. }
  443. }
  444. }
  445. case .suspend:
  446. pumpManager.suspendDelivery { (error) in
  447. if let error = error {
  448. DispatchQueue.main.async {
  449. let title = LocalizedString("Error Suspending", comment: "The alert title for a suspend error")
  450. self.present(UIAlertController(with: error, title: title), animated: true)
  451. }
  452. }
  453. }
  454. default:
  455. break
  456. }
  457. }
  458. }
  459. extension MinimedPumpSettingsViewController: PumpManagerStatusObserver {
  460. public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) {
  461. dispatchPrecondition(condition: .onQueue(.main))
  462. if let suspendResumeTableViewCell = self.tableView?.cellForRow(at: IndexPath(row: ActionsRow.suspendResume.rawValue, section: Section.actions.rawValue)) as? SuspendResumeTableViewCell
  463. {
  464. suspendResumeTableViewCell.basalDeliveryState = status.basalDeliveryState
  465. }
  466. }
  467. }
  468. private extension UIAlertController {
  469. convenience init(pumpDeletionHandler handler: @escaping () -> Void) {
  470. self.init(
  471. title: nil,
  472. message: LocalizedString("Are you sure you want to delete this pump?", comment: "Confirmation message for deleting a pump"),
  473. preferredStyle: .actionSheet
  474. )
  475. addAction(UIAlertAction(
  476. title: LocalizedString("Delete Pump", comment: "Button title to delete pump"),
  477. style: .destructive,
  478. handler: { (_) in
  479. handler()
  480. }
  481. ))
  482. let cancel = LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet")
  483. addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
  484. }
  485. }
  486. private extension UITableViewCell {
  487. func setDetailDate(_ date: Date?, formatter: DateFormatter) {
  488. if let date = date {
  489. detailTextLabel?.text = formatter.string(from: date)
  490. } else {
  491. detailTextLabel?.text = "-"
  492. }
  493. }
  494. func setTuneInfo(lastValidFrequency: Measurement<UnitFrequency>?, lastTuned: Date?, measurementFormatter: MeasurementFormatter, dateFormatter: DateFormatter) {
  495. if let frequency = lastValidFrequency, let date = lastTuned {
  496. textLabel?.text = measurementFormatter.string(from: frequency)
  497. setDetailDate(date, formatter: dateFormatter)
  498. } else {
  499. textLabel?.text = LocalizedString("Tune Radio Frequency", comment: "The title of the command to re-tune the radio")
  500. }
  501. }
  502. }