InsulinDeliveryTableViewController.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. //
  2. // InsulinDeliveryTableViewController.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 1/30/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import LoopKit
  10. private let ReuseIdentifier = "Right Detail"
  11. public final class InsulinDeliveryTableViewController: UITableViewController {
  12. @IBOutlet var needsConfigurationMessageView: ErrorBackgroundView!
  13. @IBOutlet weak var iobValueLabel: UILabel!
  14. @IBOutlet weak var iobDateLabel: UILabel!
  15. @IBOutlet weak var totalValueLabel: UILabel!
  16. @IBOutlet weak var totalDateLabel: UILabel!
  17. @IBOutlet weak var dataSourceSegmentedControl: UISegmentedControl!
  18. public var doseStore: DoseStore? {
  19. didSet {
  20. if let doseStore = doseStore {
  21. doseStoreObserver = NotificationCenter.default.addObserver(forName: nil, object: doseStore, queue: OperationQueue.main, using: { [weak self] (note) -> Void in
  22. switch note.name {
  23. case DoseStore.valuesDidChange:
  24. if self?.isViewLoaded == true {
  25. self?.reloadData()
  26. }
  27. default:
  28. break
  29. }
  30. })
  31. } else {
  32. doseStoreObserver = nil
  33. }
  34. }
  35. }
  36. private var updateTimer: Timer? {
  37. willSet {
  38. if let timer = updateTimer {
  39. timer.invalidate()
  40. }
  41. }
  42. }
  43. public override func viewDidLoad() {
  44. super.viewDidLoad()
  45. state = .display
  46. }
  47. public override func viewWillAppear(_ animated: Bool) {
  48. super.viewWillAppear(animated)
  49. updateTimelyStats(nil)
  50. }
  51. public override func viewDidAppear(_ animated: Bool) {
  52. super.viewDidAppear(animated)
  53. let updateInterval = TimeInterval(minutes: 5)
  54. let timer = Timer(
  55. fireAt: Date().dateCeiledToTimeInterval(updateInterval).addingTimeInterval(2),
  56. interval: updateInterval,
  57. target: self,
  58. selector: #selector(updateTimelyStats(_:)),
  59. userInfo: nil,
  60. repeats: true
  61. )
  62. updateTimer = timer
  63. RunLoop.current.add(timer, forMode: RunLoop.Mode.default)
  64. }
  65. public override func viewWillDisappear(_ animated: Bool) {
  66. super.viewWillDisappear(animated)
  67. updateTimer = nil
  68. }
  69. public override func viewDidDisappear(_ animated: Bool) {
  70. super.viewDidDisappear(animated)
  71. if tableView.isEditing {
  72. tableView.endEditing(true)
  73. }
  74. }
  75. deinit {
  76. if let observer = doseStoreObserver {
  77. NotificationCenter.default.removeObserver(observer)
  78. }
  79. }
  80. public override func setEditing(_ editing: Bool, animated: Bool) {
  81. super.setEditing(editing, animated: animated)
  82. if editing {
  83. let item = UIBarButtonItem(
  84. title: LocalizedString("Delete All", comment: "Button title to delete all objects"),
  85. style: .plain,
  86. target: self,
  87. action: #selector(confirmDeletion(_:))
  88. )
  89. navigationItem.setLeftBarButton(item, animated: true)
  90. } else {
  91. navigationItem.setLeftBarButton(nil, animated: true)
  92. }
  93. }
  94. // MARK: - Data
  95. private enum State {
  96. case unknown
  97. case unavailable(Error?)
  98. case display
  99. }
  100. private var state = State.unknown {
  101. didSet {
  102. if isViewLoaded {
  103. reloadData()
  104. }
  105. }
  106. }
  107. private enum DataSourceSegment: Int {
  108. case reservoir = 0
  109. case history
  110. }
  111. private enum Values {
  112. case reservoir([ReservoirValue])
  113. case history([PersistedPumpEvent])
  114. }
  115. // Not thread-safe
  116. private var values = Values.reservoir([]) {
  117. didSet {
  118. let count: Int
  119. switch values {
  120. case .reservoir(let values):
  121. count = values.count
  122. case .history(let values):
  123. count = values.count
  124. }
  125. if count > 0 {
  126. navigationItem.rightBarButtonItem = self.editButtonItem
  127. }
  128. }
  129. }
  130. private func reloadData() {
  131. switch state {
  132. case .unknown:
  133. break
  134. case .unavailable(let error):
  135. self.tableView.tableHeaderView?.isHidden = true
  136. self.tableView.tableFooterView = UIView()
  137. tableView.backgroundView = needsConfigurationMessageView
  138. if let error = error {
  139. needsConfigurationMessageView.errorDescriptionLabel.text = String(describing: error)
  140. } else {
  141. needsConfigurationMessageView.errorDescriptionLabel.text = nil
  142. }
  143. case .display:
  144. self.tableView.backgroundView = nil
  145. self.tableView.tableHeaderView?.isHidden = false
  146. self.tableView.tableFooterView = nil
  147. switch DataSourceSegment(rawValue: dataSourceSegmentedControl.selectedSegmentIndex)! {
  148. case .reservoir:
  149. doseStore?.getReservoirValues(since: Date.distantPast) { (result) in
  150. DispatchQueue.main.async { () -> Void in
  151. switch result {
  152. case .failure(let error):
  153. self.state = .unavailable(error)
  154. case .success(let reservoirValues):
  155. self.values = .reservoir(reservoirValues)
  156. self.tableView.reloadData()
  157. }
  158. }
  159. self.updateTimelyStats(nil)
  160. self.updateTotal()
  161. }
  162. case .history:
  163. doseStore?.getPumpEventValues(since: Date.distantPast) { (result) in
  164. DispatchQueue.main.async { () -> Void in
  165. switch result {
  166. case .failure(let error):
  167. self.state = .unavailable(error)
  168. case .success(let pumpEventValues):
  169. self.values = .history(pumpEventValues)
  170. self.tableView.reloadData()
  171. }
  172. }
  173. self.updateTimelyStats(nil)
  174. self.updateTotal()
  175. }
  176. }
  177. }
  178. }
  179. @objc func updateTimelyStats(_: Timer?) {
  180. updateIOB()
  181. }
  182. private lazy var iobNumberFormatter: NumberFormatter = {
  183. let formatter = NumberFormatter()
  184. formatter.numberStyle = .decimal
  185. formatter.maximumFractionDigits = 2
  186. return formatter
  187. }()
  188. private lazy var timeFormatter: DateFormatter = {
  189. let formatter = DateFormatter()
  190. formatter.dateStyle = .none
  191. formatter.timeStyle = .short
  192. return formatter
  193. }()
  194. private func updateIOB() {
  195. if case .display = state {
  196. doseStore?.insulinOnBoard(at: Date()) { (result) -> Void in
  197. DispatchQueue.main.async {
  198. switch result {
  199. case .failure:
  200. self.iobValueLabel.text = "…"
  201. self.iobDateLabel.text = nil
  202. case .success(let iob):
  203. self.iobValueLabel.text = self.iobNumberFormatter.string(from: iob.value)
  204. self.iobDateLabel.text = String(format: LocalizedString("com.loudnate.InsulinKit.IOBDateLabel", value: "at %1$@", comment: "The format string describing the date of an IOB value. The first format argument is the localized date."), self.timeFormatter.string(from: iob.startDate))
  205. }
  206. }
  207. }
  208. }
  209. }
  210. private func updateTotal() {
  211. if case .display = state {
  212. let midnight = Calendar.current.startOfDay(for: Date())
  213. doseStore?.getTotalUnitsDelivered(since: midnight) { (result) in
  214. DispatchQueue.main.async {
  215. switch result {
  216. case .failure:
  217. self.totalValueLabel.text = "…"
  218. self.totalDateLabel.text = nil
  219. case .success(let result):
  220. self.totalValueLabel.text = NumberFormatter.localizedString(from: NSNumber(value: result.value), number: .none)
  221. self.totalDateLabel.text = String(format: LocalizedString("com.loudnate.InsulinKit.totalDateLabel", value: "since %1$@", comment: "The format string describing the starting date of a total value. The first format argument is the localized date."), DateFormatter.localizedString(from: result.startDate, dateStyle: .none, timeStyle: .short))
  222. }
  223. }
  224. }
  225. }
  226. }
  227. private var doseStoreObserver: Any? {
  228. willSet {
  229. if let observer = doseStoreObserver {
  230. NotificationCenter.default.removeObserver(observer)
  231. }
  232. }
  233. }
  234. @IBAction func selectedSegmentChanged(_ sender: Any) {
  235. reloadData()
  236. }
  237. @IBAction func confirmDeletion(_ sender: Any) {
  238. guard !deletionPending else {
  239. return
  240. }
  241. let confirmMessage: String
  242. switch DataSourceSegment(rawValue: dataSourceSegmentedControl.selectedSegmentIndex)! {
  243. case .reservoir:
  244. confirmMessage = LocalizedString("Are you sure you want to delete all reservoir values?", comment: "Action sheet confirmation message for reservoir deletion")
  245. case .history:
  246. confirmMessage = LocalizedString("Are you sure you want to delete all history entries?", comment: "Action sheet confirmation message for pump history deletion")
  247. }
  248. let sheet = UIAlertController(deleteAllConfirmationMessage: confirmMessage) {
  249. self.deleteAllObjects()
  250. }
  251. present(sheet, animated: true)
  252. }
  253. private var deletionPending = false
  254. private func deleteAllObjects() {
  255. guard !deletionPending else {
  256. return
  257. }
  258. deletionPending = true
  259. let completion = { (_: DoseStore.DoseStoreError?) -> Void in
  260. DispatchQueue.main.async {
  261. self.deletionPending = false
  262. self.setEditing(false, animated: true)
  263. }
  264. }
  265. switch DataSourceSegment(rawValue: dataSourceSegmentedControl.selectedSegmentIndex)! {
  266. case .reservoir:
  267. doseStore?.deleteAllReservoirValues(completion)
  268. case .history:
  269. doseStore?.deleteAllPumpEvents(completion)
  270. }
  271. }
  272. // MARK: - Table view data source
  273. public override func numberOfSections(in tableView: UITableView) -> Int {
  274. switch state {
  275. case .unknown, .unavailable:
  276. return 0
  277. case .display:
  278. return 1
  279. }
  280. }
  281. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  282. switch values {
  283. case .reservoir(let values):
  284. return values.count
  285. case .history(let values):
  286. return values.count
  287. }
  288. }
  289. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  290. let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier, for: indexPath)
  291. if case .display = state {
  292. switch self.values {
  293. case .reservoir(let values):
  294. let entry = values[indexPath.row]
  295. let volume = NumberFormatter.localizedString(from: NSNumber(value: entry.unitVolume), number: .decimal)
  296. let time = timeFormatter.string(from: entry.startDate)
  297. cell.textLabel?.text = "\(volume) U"
  298. cell.detailTextLabel?.text = time
  299. cell.accessoryType = .none
  300. cell.selectionStyle = .none
  301. case .history(let values):
  302. let entry = values[indexPath.row]
  303. let time = timeFormatter.string(from: entry.date)
  304. cell.textLabel?.text = entry.title ?? LocalizedString("Unknown", comment: "The default title to use when an entry has none")
  305. cell.detailTextLabel?.text = time
  306. cell.accessoryType = entry.isUploaded ? .checkmark : .none
  307. cell.selectionStyle = .default
  308. }
  309. }
  310. return cell
  311. }
  312. public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  313. return true
  314. }
  315. public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  316. if editingStyle == .delete, case .display = state {
  317. switch values {
  318. case .reservoir(let reservoirValues):
  319. var reservoirValues = reservoirValues
  320. let value = reservoirValues.remove(at: indexPath.row)
  321. self.values = .reservoir(reservoirValues)
  322. tableView.deleteRows(at: [indexPath], with: .automatic)
  323. doseStore?.deleteReservoirValue(value) { (_, error) -> Void in
  324. if let error = error {
  325. DispatchQueue.main.async {
  326. self.present(UIAlertController(with: error), animated: true)
  327. self.reloadData()
  328. }
  329. }
  330. }
  331. case .history(let historyValues):
  332. var historyValues = historyValues
  333. let value = historyValues.remove(at: indexPath.row)
  334. self.values = .history(historyValues)
  335. tableView.deleteRows(at: [indexPath], with: .automatic)
  336. doseStore?.deletePumpEvent(value) { (error) -> Void in
  337. if let error = error {
  338. DispatchQueue.main.async {
  339. self.present(UIAlertController(with: error), animated: true)
  340. self.reloadData()
  341. }
  342. }
  343. }
  344. }
  345. }
  346. }
  347. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  348. if case .display = state, case .history(let history) = values {
  349. let entry = history[indexPath.row]
  350. let vc = CommandResponseViewController(command: { (completionHandler) -> String in
  351. var description = [String]()
  352. description.append(self.timeFormatter.string(from: entry.date))
  353. if let title = entry.title {
  354. description.append(title)
  355. }
  356. if let dose = entry.dose {
  357. description.append(String(describing: dose))
  358. }
  359. if let raw = entry.raw {
  360. description.append(raw.hexadecimalString)
  361. }
  362. return description.joined(separator: "\n\n")
  363. })
  364. vc.title = LocalizedString("Pump Event", comment: "The title of the screen displaying a pump event")
  365. show(vc, sender: indexPath)
  366. }
  367. }
  368. }
  369. fileprivate extension UIAlertController {
  370. convenience init(deleteAllConfirmationMessage: String, confirmationHandler handler: @escaping () -> Void) {
  371. self.init(
  372. title: nil,
  373. message: deleteAllConfirmationMessage,
  374. preferredStyle: .actionSheet
  375. )
  376. addAction(UIAlertAction(
  377. title: LocalizedString("Delete All", comment: "Button title to delete all objects"),
  378. style: .destructive,
  379. handler: { (_) in handler() }
  380. ))
  381. addAction(UIAlertAction(
  382. title: LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"),
  383. style: .cancel
  384. ))
  385. }
  386. }