GlucoseRangeScheduleTableViewController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. //
  2. // GlucoseRangeScheduleTableViewController.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 2/13/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. public enum SaveGlucoseRangeScheduleResult {
  12. case success
  13. case failure(Error)
  14. }
  15. public protocol GlucoseRangeScheduleStorageDelegate {
  16. func saveSchedule(for viewController: GlucoseRangeScheduleTableViewController, completion: @escaping (_ result: SaveGlucoseRangeScheduleResult) -> Void)
  17. }
  18. private struct EditableRange {
  19. public let minValue: Double?
  20. public let maxValue: Double?
  21. init(minValue: Double?, maxValue: Double?) {
  22. self.minValue = minValue
  23. self.maxValue = maxValue
  24. }
  25. }
  26. public class GlucoseRangeScheduleTableViewController: UITableViewController {
  27. public init(allowedValues: [Double], unit: HKUnit, minimumTimeInterval: TimeInterval = TimeInterval(30 * 60)) {
  28. self.allowedValues = allowedValues
  29. self.unit = unit
  30. self.minimumTimeInterval = minimumTimeInterval
  31. super.init(style: .grouped)
  32. }
  33. public required init?(coder aDecoder: NSCoder) {
  34. fatalError("init(coder:) has not been implemented")
  35. }
  36. public override func viewDidLoad() {
  37. super.viewDidLoad()
  38. tableView.register(GlucoseRangeTableViewCell.nib(), forCellReuseIdentifier: GlucoseRangeTableViewCell.className)
  39. tableView.register(GlucoseRangeOverrideTableViewCell.nib(), forCellReuseIdentifier: GlucoseRangeOverrideTableViewCell.className)
  40. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  41. navigationItem.rightBarButtonItems = [insertButtonItem, editButtonItem]
  42. updateEditButton()
  43. }
  44. @objc private func cancel(_ sender: Any?) {
  45. self.navigationController?.popViewController(animated: true)
  46. }
  47. public private(set) lazy var insertButtonItem: UIBarButtonItem = {
  48. return UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addScheduleItem(_:)))
  49. }()
  50. private func updateInsertButton() {
  51. guard let lastItem = editableItems.last else {
  52. return
  53. }
  54. insertButtonItem.isEnabled = !isEditing && lastItem.startTime < lastValidStartTime
  55. }
  56. open override func setEditing(_ editing: Bool, animated: Bool) {
  57. tableView.beginUpdates()
  58. hideGlucoseRangeCells()
  59. tableView.endUpdates()
  60. super.setEditing(editing, animated: animated)
  61. updateInsertButton()
  62. updateSaveButton()
  63. }
  64. private func updateSaveButton() {
  65. if let section = sections.firstIndex(of: .save), let cell = tableView.cellForRow(at: IndexPath(row: 0, section: section)) as? TextButtonTableViewCell {
  66. cell.isEnabled = !isEditing && isScheduleModified && isScheduleValid
  67. }
  68. }
  69. private var isScheduleValid: Bool {
  70. return !editableItems.isEmpty &&
  71. editableItems.allSatisfy { isValid($0.value) }
  72. }
  73. private func updateEditButton() {
  74. editButtonItem.isEnabled = editableItems.endIndex > 1
  75. }
  76. public func setSchedule(_ schedule: GlucoseRangeSchedule, withOverrideRanges overrides: [TemporaryScheduleOverride.Context: DoubleRange]) {
  77. unit = schedule.unit
  78. editableItems = schedule.items.map({ (item) -> RepeatingScheduleValue<EditableRange> in
  79. let range = EditableRange(minValue: item.value.minValue, maxValue: item.value.maxValue)
  80. return RepeatingScheduleValue<EditableRange>(startTime: item.startTime, value: range)
  81. })
  82. editableOverrideRanges.removeAll()
  83. for (context, range) in overrides {
  84. editableOverrideRanges[context] = EditableRange(minValue: range.minValue, maxValue: range.maxValue)
  85. }
  86. isScheduleModified = false
  87. }
  88. private func isValid(_ range: EditableRange) -> Bool {
  89. guard let max = range.maxValue, let min = range.minValue, min <= max else {
  90. return false
  91. }
  92. return allowedValues.contains(max) && allowedValues.contains(min)
  93. }
  94. @IBAction func addScheduleItem(_ sender: Any?) {
  95. guard let allowedTimeRange = allowedTimeRange(for: editableItems.count) else {
  96. return
  97. }
  98. editableItems.append(
  99. RepeatingScheduleValue(
  100. startTime: allowedTimeRange.lowerBound,
  101. value: editableItems.last?.value ?? EditableRange(minValue: nil, maxValue: nil)
  102. )
  103. )
  104. tableView.beginUpdates()
  105. tableView.insertRows(at: [IndexPath(row: editableItems.count - 1, section: Section.schedule.rawValue)], with: .automatic)
  106. if editableItems.count == 1 {
  107. tableView.insertSections(IndexSet(integer: Section.override.rawValue), with: .automatic)
  108. }
  109. tableView.endUpdates()
  110. }
  111. private func updateTimeLimits(for index: Int) {
  112. let indexPath = IndexPath(row: index, section: Section.schedule.rawValue)
  113. if let allowedTimeRange = allowedTimeRange(for: index), let cell = tableView.cellForRow(at: indexPath) as? GlucoseRangeTableViewCell {
  114. cell.allowedTimeRange = allowedTimeRange
  115. }
  116. }
  117. private func allowedTimeRange(for index: Int) -> ClosedRange<TimeInterval>? {
  118. let minTime: TimeInterval
  119. let maxTime: TimeInterval
  120. if index == 0 {
  121. maxTime = TimeInterval(0)
  122. } else if index+1 < editableItems.endIndex {
  123. maxTime = editableItems[index+1].startTime - minimumTimeInterval
  124. } else {
  125. maxTime = lastValidStartTime
  126. }
  127. if index > 0 {
  128. minTime = editableItems[index-1].startTime + minimumTimeInterval
  129. if minTime > lastValidStartTime {
  130. return nil
  131. }
  132. } else {
  133. minTime = TimeInterval(0)
  134. }
  135. return minTime...maxTime
  136. }
  137. func insertableIndices(removing row: Int) -> [Bool] {
  138. let insertableIndices = editableItems.enumerated().map { (enumeration) -> Bool in
  139. let (index, item) = enumeration
  140. if row == index {
  141. return true
  142. } else if index == 0 {
  143. return false
  144. } else if index == editableItems.endIndex - 1 {
  145. return item.startTime < TimeInterval(hours: 24) - minimumTimeInterval
  146. } else if index > row {
  147. return editableItems[index + 1].startTime - item.startTime > minimumTimeInterval
  148. } else {
  149. return item.startTime - editableItems[index - 1].startTime > minimumTimeInterval
  150. }
  151. }
  152. return insertableIndices
  153. }
  154. // MARK: - State
  155. public var delegate: GlucoseRangeScheduleStorageDelegate?
  156. let allowedValues: [Double]
  157. let minimumTimeInterval: TimeInterval
  158. var lastValidStartTime: TimeInterval {
  159. return TimeInterval.hours(24) - minimumTimeInterval
  160. }
  161. public var timeZone = TimeZone.currentFixed
  162. private var unit: HKUnit = HKUnit.milligramsPerDeciliter
  163. private var isScheduleModified = false {
  164. didSet {
  165. if isScheduleModified {
  166. self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(_:)))
  167. } else {
  168. self.navigationItem.leftBarButtonItem = nil
  169. }
  170. updateSaveButton()
  171. }
  172. }
  173. private var editableItems: [RepeatingScheduleValue<EditableRange>] = [] {
  174. didSet {
  175. isScheduleModified = true
  176. updateInsertButton()
  177. updateEditButton()
  178. }
  179. }
  180. public var schedule: GlucoseRangeSchedule? {
  181. get {
  182. let dailyItems = editableItems.compactMap { (item) -> RepeatingScheduleValue<DoubleRange>? in
  183. guard isValid(item.value) else {
  184. return nil
  185. }
  186. guard let min = item.value.minValue, let max = item.value.maxValue else {
  187. return nil
  188. }
  189. let range = DoubleRange(minValue: min, maxValue: max)
  190. return RepeatingScheduleValue(startTime: item.startTime, value: range)
  191. }
  192. return GlucoseRangeSchedule(unit: unit, dailyItems: dailyItems)
  193. }
  194. }
  195. public var overrideContexts: [TemporaryScheduleOverride.Context] = [.preMeal, .legacyWorkout]
  196. private var editableOverrideRanges: [TemporaryScheduleOverride.Context: EditableRange] = [:] {
  197. didSet {
  198. isScheduleModified = true
  199. }
  200. }
  201. public var overrideRanges: [TemporaryScheduleOverride.Context: DoubleRange] {
  202. get {
  203. var setRanges: [TemporaryScheduleOverride.Context: DoubleRange] = [:]
  204. for (context, range) in editableOverrideRanges {
  205. if let minValue = range.minValue, let maxValue = range.maxValue, isValid(range) {
  206. setRanges[context] = DoubleRange(minValue: minValue, maxValue: maxValue)
  207. }
  208. }
  209. return setRanges
  210. }
  211. }
  212. // MARK: - UITableViewDataSource
  213. private enum Section: Int, CaseIterable {
  214. case schedule = 0
  215. case override
  216. case save
  217. }
  218. private var showOverrides: Bool {
  219. return !editableItems.isEmpty
  220. }
  221. private var sections: [Section] {
  222. if !showOverrides {
  223. return [.schedule, .save]
  224. } else {
  225. return Section.allCases
  226. }
  227. }
  228. public override func numberOfSections(in tableView: UITableView) -> Int {
  229. return sections.count
  230. }
  231. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  232. switch sections[section] {
  233. case .schedule:
  234. return editableItems.count
  235. case .override:
  236. return overrideContexts.count
  237. case .save:
  238. return 1
  239. }
  240. }
  241. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  242. switch sections[indexPath.section] {
  243. case .schedule:
  244. let cell = tableView.dequeueReusableCell(withIdentifier: GlucoseRangeTableViewCell.className, for: indexPath) as! GlucoseRangeTableViewCell
  245. let item = editableItems[indexPath.row]
  246. cell.timeZone = timeZone
  247. cell.startTime = item.startTime
  248. cell.allowedValues = allowedValues
  249. cell.valueNumberFormatter.minimumFractionDigits = unit.preferredFractionDigits
  250. cell.valueNumberFormatter.maximumFractionDigits = unit.preferredFractionDigits
  251. cell.minValue = item.value.minValue
  252. cell.maxValue = item.value.maxValue
  253. cell.unitString = unit.shortLocalizedUnitString()
  254. cell.delegate = self
  255. if let allowedTimeRange = allowedTimeRange(for: indexPath.row) {
  256. cell.allowedTimeRange = allowedTimeRange
  257. }
  258. return cell
  259. case .override:
  260. let cell = tableView.dequeueReusableCell(withIdentifier: GlucoseRangeOverrideTableViewCell.className, for: indexPath) as! GlucoseRangeOverrideTableViewCell
  261. cell.valueNumberFormatter.minimumFractionDigits = unit.preferredFractionDigits
  262. cell.valueNumberFormatter.maximumFractionDigits = unit.preferredFractionDigits
  263. cell.allowedValues = allowedValues
  264. let context = overrideContexts[indexPath.row]
  265. if let range = overrideRanges[context], !range.isZero {
  266. cell.minValue = range.minValue
  267. cell.maxValue = range.maxValue
  268. }
  269. let bundle = Bundle(for: type(of: self))
  270. let titleText: String
  271. let image: UIImage?
  272. switch context {
  273. case .legacyWorkout:
  274. titleText = LocalizedString("Workout", comment: "Title for the workout override range")
  275. image = UIImage(named: "workout", in: bundle, compatibleWith: traitCollection)
  276. case .preMeal:
  277. titleText = LocalizedString("Pre-Meal", comment: "Title for the pre-meal override range")
  278. image = UIImage(named: "Pre-Meal", in: bundle, compatibleWith: traitCollection)
  279. default:
  280. preconditionFailure("Unexpected override context \(context)")
  281. }
  282. cell.dateLabel.text = titleText
  283. cell.iconImageView.image = image
  284. cell.unitString = unit.shortLocalizedUnitString()
  285. cell.delegate = self
  286. return cell
  287. case .save:
  288. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  289. cell.textLabel?.text = LocalizedString("Save", comment: "Button text for saving glucose correction range schedule")
  290. cell.isEnabled = isScheduleModified
  291. return cell
  292. }
  293. }
  294. public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  295. if editingStyle == .delete, let overrideSectionIndex = sections.firstIndex(of: .override) {
  296. editableItems.remove(at: indexPath.row)
  297. tableView.performBatchUpdates({
  298. tableView.deleteRows(at: [indexPath], with: .automatic)
  299. if editableItems.isEmpty {
  300. tableView.deleteSections(IndexSet(integer: overrideSectionIndex), with: .automatic)
  301. }
  302. }, completion: nil)
  303. if editableItems.count == 1 {
  304. setEditing(false, animated: true)
  305. }
  306. updateSaveButton()
  307. }
  308. }
  309. public override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  310. if sourceIndexPath != destinationIndexPath {
  311. switch sections[destinationIndexPath.section] {
  312. case .schedule:
  313. let item = editableItems.remove(at: sourceIndexPath.row)
  314. editableItems.insert(item, at: destinationIndexPath.row)
  315. guard destinationIndexPath.row > 0 else {
  316. return
  317. }
  318. let startTime = editableItems[destinationIndexPath.row - 1].startTime + minimumTimeInterval
  319. editableItems[destinationIndexPath.row] = RepeatingScheduleValue(startTime: startTime, value: editableItems[destinationIndexPath.row].value)
  320. // Since the valid date ranges of neighboring cells are affected, the lazy solution is to just reload the entire table view
  321. DispatchQueue.main.async {
  322. tableView.reloadData()
  323. }
  324. case .override, .save:
  325. break
  326. }
  327. }
  328. }
  329. public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  330. switch sections[indexPath.section] {
  331. case .schedule:
  332. return indexPath.row > 0
  333. default:
  334. return false
  335. }
  336. }
  337. public override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  338. switch sections[indexPath.section] {
  339. case .schedule:
  340. return true
  341. default:
  342. return false
  343. }
  344. }
  345. public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
  346. switch sections[section] {
  347. case .override:
  348. return LocalizedString("Overrides", comment: "The section title of glucose overrides")
  349. default:
  350. return nil
  351. }
  352. }
  353. public override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  354. switch sections[section] {
  355. case .schedule:
  356. return LocalizedString("Correction range is the blood glucose range that you would like Loop to correct to.", comment: "The section footer of correction range schedule")
  357. default:
  358. return nil
  359. }
  360. }
  361. // MARK: - UITableViewDelegate
  362. public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  363. return true
  364. }
  365. public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  366. tableView.beginUpdates()
  367. switch sections[indexPath.section] {
  368. case .schedule:
  369. updateTimeLimits(for: indexPath.row)
  370. hideGlucoseRangeCells(excluding: indexPath)
  371. case .override:
  372. hideGlucoseRangeCells(excluding: indexPath)
  373. default:
  374. break
  375. }
  376. tableView.endEditing(false)
  377. return indexPath
  378. }
  379. public override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
  380. tableView.beginUpdates()
  381. tableView.endUpdates()
  382. }
  383. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  384. switch sections[indexPath.section] {
  385. case .save:
  386. delegate?.saveSchedule(for: self, completion: { (result) in
  387. switch result {
  388. case .success:
  389. self.isScheduleModified = false
  390. self.updateInsertButton()
  391. case .failure(let error):
  392. self.present(UIAlertController(with: error), animated: true)
  393. }
  394. })
  395. default:
  396. break
  397. }
  398. tableView.endEditing(false)
  399. tableView.endUpdates()
  400. tableView.deselectRow(at: indexPath, animated: true)
  401. }
  402. public override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
  403. guard sourceIndexPath.section == proposedDestinationIndexPath.section else {
  404. return sourceIndexPath
  405. }
  406. guard sourceIndexPath != proposedDestinationIndexPath else {
  407. return proposedDestinationIndexPath
  408. }
  409. let indices = insertableIndices(removing: sourceIndexPath.row)
  410. let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
  411. return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
  412. }
  413. }
  414. extension GlucoseRangeScheduleTableViewController : GlucoseRangeTableViewCellDelegate {
  415. func glucoseRangeTableViewCellDidUpdate(_ cell: GlucoseRangeTableViewCell) {
  416. if let indexPath = tableView.indexPath(for: cell) {
  417. switch sections[indexPath.section] {
  418. case .schedule:
  419. editableItems[indexPath.row] = RepeatingScheduleValue(
  420. startTime: cell.startTime,
  421. value: EditableRange(minValue: cell.minValue, maxValue: cell.maxValue)
  422. )
  423. case .override:
  424. let context = overrideContexts[indexPath.row]
  425. editableOverrideRanges[context] = EditableRange(minValue: cell.minValue, maxValue: cell.maxValue)
  426. default:
  427. break
  428. }
  429. }
  430. }
  431. }