InsulinSensitivityScheduleViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. //
  2. // InsulinSensitivityScheduleViewController.swift
  3. // LoopKitUI
  4. //
  5. // Created by Pete Schwamb on 7/2/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. public enum SaveInsulinSensitivityScheduleResult {
  12. case success
  13. case failure(Error)
  14. }
  15. public protocol InsulinSensitivityScheduleStorageDelegate {
  16. func saveSchedule(_ schedule: InsulinSensitivitySchedule, for viewController: InsulinSensitivityScheduleViewController, completion: @escaping (_ result: SaveInsulinSensitivityScheduleResult) -> Void)
  17. }
  18. public class InsulinSensitivityScheduleViewController : DailyValueScheduleTableViewController {
  19. public init(allowedValues: [Double], unit: HKUnit, minimumTimeInterval: TimeInterval = TimeInterval(30 * 60)) {
  20. self.allowedValues = allowedValues
  21. self.minimumTimeInterval = minimumTimeInterval
  22. self.unit = unit
  23. super.init(style: .grouped)
  24. }
  25. public required init?(coder aDecoder: NSCoder) {
  26. fatalError("init(coder:) has not been implemented")
  27. }
  28. open override func viewDidLoad() {
  29. super.viewDidLoad()
  30. tableView.register(SetConstrainedScheduleEntryTableViewCell.nib(), forCellReuseIdentifier: SetConstrainedScheduleEntryTableViewCell.className)
  31. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  32. updateEditButton()
  33. }
  34. @objc private func cancel(_ sender: Any?) {
  35. self.navigationController?.popViewController(animated: true)
  36. }
  37. // MARK: - State
  38. public var insulinSensitivityScheduleStorageDelegate: InsulinSensitivityScheduleStorageDelegate?
  39. public var unit: HKUnit = HKUnit.milligramsPerDeciliter.unitDivided(by: .internationalUnit())
  40. public var schedule: InsulinSensitivitySchedule? {
  41. get {
  42. let validEntries = internalItems.compactMap { (item) -> RepeatingScheduleValue<Double>? in
  43. guard let value = item.value else {
  44. return nil
  45. }
  46. return RepeatingScheduleValue(startTime: item.startTime, value: value)
  47. }
  48. return InsulinSensitivitySchedule(unit: unit, dailyItems: validEntries, timeZone: timeZone)
  49. }
  50. set {
  51. if let newValue = newValue {
  52. unit = newValue.unit
  53. internalItems = newValue.items.map { (entry) -> RepeatingScheduleValue<Double?> in
  54. RepeatingScheduleValue(startTime: entry.startTime, value: entry.value)
  55. }
  56. isScheduleModified = false
  57. } else {
  58. internalItems = []
  59. }
  60. }
  61. }
  62. private var internalItems: [RepeatingScheduleValue<Double?>] = [] {
  63. didSet {
  64. isScheduleModified = true
  65. updateInsertButton()
  66. }
  67. }
  68. let allowedValues: [Double]
  69. let minimumTimeInterval: TimeInterval
  70. var lastValidStartTime: TimeInterval {
  71. return TimeInterval.hours(24) - minimumTimeInterval
  72. }
  73. private var isScheduleModified = false {
  74. didSet {
  75. updateCancelButton()
  76. updateSaveButton()
  77. }
  78. }
  79. private func isValid(_ value: Double?) -> Bool {
  80. guard let value = value else {
  81. return false
  82. }
  83. return allowedValues.contains(value)
  84. }
  85. public var isScheduleValid: Bool {
  86. return !internalItems.isEmpty &&
  87. internalItems.allSatisfy { isValid($0.value) }
  88. }
  89. private func updateCancelButton() {
  90. if isScheduleModified {
  91. self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(_:)))
  92. } else {
  93. self.navigationItem.leftBarButtonItem = nil
  94. }
  95. }
  96. private func updateSaveButton() {
  97. if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: Section.save.rawValue)) as? TextButtonTableViewCell {
  98. cell.isEnabled = !isEditing && isScheduleModified && isScheduleValid
  99. }
  100. }
  101. private func updateEditButton() {
  102. editButtonItem.isEnabled = internalItems.endIndex > 1
  103. }
  104. private func updateInsertButton() {
  105. guard let lastItem = internalItems.last else {
  106. return
  107. }
  108. insertButtonItem.isEnabled = !isEditing && lastItem.startTime < lastValidStartTime
  109. }
  110. override func addScheduleItem(_ sender: Any?) {
  111. tableView.endEditing(false)
  112. let startTime: TimeInterval
  113. let value: Double?
  114. if let lastItem = internalItems.last {
  115. startTime = lastItem.startTime + minimumTimeInterval
  116. value = lastItem.value
  117. if startTime > lastValidStartTime {
  118. return
  119. }
  120. } else {
  121. startTime = TimeInterval(0)
  122. value = nil
  123. }
  124. internalItems.append(
  125. RepeatingScheduleValue(
  126. startTime: startTime,
  127. value: value
  128. )
  129. )
  130. updateTimeLimitsForItemsAdjacent(to: internalItems.endIndex-1)
  131. super.addScheduleItem(sender)
  132. if internalItems.count == 1 {
  133. let index = IndexPath(row: 0, section: Section.schedule.rawValue)
  134. tableView.beginUpdates()
  135. tableView.selectRow(at: index, animated: true, scrollPosition: .top)
  136. tableView.deselectRow(at: index, animated: true)
  137. tableView.endUpdates()
  138. } else {
  139. tableView.beginUpdates()
  140. hideSetConstrainedScheduleEntryCells()
  141. tableView.endUpdates()
  142. }
  143. updateEditButton()
  144. }
  145. override func insertableIndiciesByRemovingRow(_ row: Int, withInterval timeInterval: TimeInterval) -> [Bool] {
  146. return insertableIndices(for: internalItems, removing: row, with: timeInterval)
  147. }
  148. open override func setEditing(_ editing: Bool, animated: Bool) {
  149. tableView.beginUpdates()
  150. hideSetConstrainedScheduleEntryCells()
  151. tableView.endUpdates()
  152. super.setEditing(editing, animated: animated)
  153. updateInsertButton()
  154. updateSaveButton()
  155. }
  156. private func updateTimeLimitsFor(itemAt index: Int) {
  157. guard internalItems.indices.contains(index) else {
  158. return
  159. }
  160. let indexPath = IndexPath(row: index, section: Section.schedule.rawValue)
  161. if let cell = tableView.cellForRow(at: indexPath) as? SetConstrainedScheduleEntryTableViewCell {
  162. if index+1 < internalItems.endIndex {
  163. cell.maximumStartTime = internalItems[index+1].startTime - minimumTimeInterval
  164. } else {
  165. cell.maximumStartTime = lastValidStartTime
  166. }
  167. if index > 1 {
  168. cell.minimumStartTime = internalItems[index-1].startTime + minimumTimeInterval
  169. }
  170. }
  171. }
  172. private func updateTimeLimitsForItemsAdjacent(to index: Int) {
  173. updateTimeLimitsFor(itemAt: index-1)
  174. updateTimeLimitsFor(itemAt: index+1)
  175. }
  176. // MARK: - UITableViewDataSource
  177. private enum Section: Int, CaseIterable {
  178. case schedule
  179. case save
  180. }
  181. open override func numberOfSections(in tableView: UITableView) -> Int {
  182. return Section.allCases.count
  183. }
  184. open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  185. switch Section(rawValue: section)! {
  186. case .schedule:
  187. return internalItems.endIndex
  188. case .save:
  189. return 1
  190. }
  191. }
  192. open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  193. switch Section(rawValue: indexPath.section)! {
  194. case .schedule:
  195. let cell = tableView.dequeueReusableCell(withIdentifier: SetConstrainedScheduleEntryTableViewCell.className, for: indexPath) as! SetConstrainedScheduleEntryTableViewCell
  196. cell.unit = unit.unitDivided(by: .internationalUnit())
  197. let item = internalItems[indexPath.row]
  198. cell.allowedValues = allowedValues
  199. cell.minimumTimeInterval = minimumTimeInterval
  200. cell.isReadOnly = false
  201. cell.isPickerHidden = true
  202. cell.delegate = self
  203. cell.timeZone = timeZone
  204. if indexPath.row > 0 {
  205. let lastItem = internalItems[indexPath.row - 1]
  206. cell.minimumStartTime = lastItem.startTime + minimumTimeInterval
  207. }
  208. if indexPath.row == 0 {
  209. cell.maximumStartTime = 0
  210. } else if indexPath.row < internalItems.endIndex - 1 {
  211. let nextItem = internalItems[indexPath.row + 1]
  212. cell.maximumStartTime = nextItem.startTime - minimumTimeInterval
  213. } else {
  214. cell.maximumStartTime = lastValidStartTime
  215. }
  216. cell.value = item.value
  217. cell.startTime = item.startTime
  218. cell.emptySelectionType = .lastIndex
  219. return cell
  220. case .save:
  221. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  222. cell.textLabel?.text = LocalizedString("Save", comment: "Button text for saving insulin sensitivity schedule")
  223. cell.isEnabled = isScheduleModified && isScheduleValid
  224. return cell
  225. }
  226. }
  227. open override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  228. switch Section(rawValue: section)! {
  229. case .schedule:
  230. return LocalizedString("Insulin sensitivity describes how your blood glucose should respond to a 1 Unit dose of insulin. Smaller values mean more insulin will be given when above target. Values that are too small can cause dangerously low blood glucose.", comment: "The description shown on the insulin sensitivity schedule interface.")
  231. case .save:
  232. return nil
  233. }
  234. }
  235. open override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  236. if editingStyle == .delete {
  237. internalItems.remove(at: indexPath.row)
  238. super.tableView(tableView, commit: editingStyle, forRowAt: indexPath)
  239. if internalItems.count == 1 {
  240. self.isEditing = false
  241. }
  242. updateInsertButton()
  243. updateEditButton()
  244. updateTimeLimitsFor(itemAt: indexPath.row-1)
  245. updateTimeLimitsFor(itemAt: indexPath.row)
  246. updateSaveButton()
  247. }
  248. }
  249. open override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  250. if sourceIndexPath != destinationIndexPath {
  251. let item = internalItems.remove(at: sourceIndexPath.row)
  252. internalItems.insert(item, at: destinationIndexPath.row)
  253. let startTime = internalItems[destinationIndexPath.row - 1].startTime + minimumTimeInterval
  254. internalItems[destinationIndexPath.row] = RepeatingScheduleValue(startTime: startTime, value: internalItems[destinationIndexPath.row].value)
  255. DispatchQueue.main.async {
  256. tableView.reloadRows(at: [destinationIndexPath], with: .none)
  257. self.updateTimeLimitsForItemsAdjacent(to: destinationIndexPath.row)
  258. }
  259. }
  260. }
  261. // MARK: - UITableViewDelegate
  262. open override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  263. guard indexPath.section == Section.schedule.rawValue else {
  264. return super.tableView(tableView, shouldHighlightRowAt: indexPath)
  265. }
  266. return !isReadOnly
  267. }
  268. open override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  269. tableView.beginUpdates()
  270. hideSetConstrainedScheduleEntryCells(excluding: indexPath)
  271. tableView.endUpdates()
  272. return super.tableView(tableView, willSelectRowAt: indexPath)
  273. }
  274. open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  275. super.tableView(tableView, didSelectRowAt: indexPath)
  276. switch Section(rawValue: indexPath.section)! {
  277. case .schedule:
  278. break
  279. case .save:
  280. if let schedule = schedule {
  281. insulinSensitivityScheduleStorageDelegate?.saveSchedule(schedule, for: self, completion: { (result) in
  282. switch result {
  283. case .success:
  284. self.delegate?.dailyValueScheduleTableViewControllerWillFinishUpdating(self)
  285. self.isScheduleModified = false
  286. self.updateInsertButton()
  287. case .failure(let error):
  288. self.present(UIAlertController(with: error), animated: true)
  289. }
  290. })
  291. }
  292. }
  293. }
  294. open override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
  295. guard sourceIndexPath != proposedDestinationIndexPath, let cell = tableView.cellForRow(at: sourceIndexPath) as? SetConstrainedScheduleEntryTableViewCell else {
  296. return proposedDestinationIndexPath
  297. }
  298. let interval = cell.minimumTimeInterval
  299. let indices = insertableIndices(for: internalItems, removing: sourceIndexPath.row, with: interval)
  300. let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
  301. return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
  302. }
  303. }
  304. extension InsulinSensitivityScheduleViewController: SetConstrainedScheduleEntryTableViewCellDelegate {
  305. func setConstrainedScheduleEntryTableViewCellDidUpdate(_ cell: SetConstrainedScheduleEntryTableViewCell) {
  306. if let indexPath = tableView.indexPath(for: cell) {
  307. internalItems[indexPath.row] = RepeatingScheduleValue(
  308. startTime: cell.startTime,
  309. value: cell.value
  310. )
  311. updateTimeLimitsForItemsAdjacent(to: indexPath.row)
  312. }
  313. }
  314. }