BasalScheduleTableViewController.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. //
  2. // BasalScheduleTableViewController.swift
  3. // LoopKitUI
  4. //
  5. // Created by Pete Schwamb on 2/23/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. public enum SyncBasalScheduleResult<T: RawRepresentable> {
  12. case success(scheduleItems: [RepeatingScheduleValue<T>], timeZone: TimeZone)
  13. case failure(Error)
  14. }
  15. public protocol BasalScheduleTableViewControllerSyncSource: AnyObject {
  16. func syncScheduleValues(for viewController: BasalScheduleTableViewController, completion: @escaping (_ result: SyncBasalScheduleResult<Double>) -> Void)
  17. func syncButtonTitle(for viewController: BasalScheduleTableViewController) -> String
  18. func syncButtonDetailText(for viewController: BasalScheduleTableViewController) -> String?
  19. func basalScheduleTableViewControllerIsReadOnly(_ viewController: BasalScheduleTableViewController) -> Bool
  20. }
  21. open class BasalScheduleTableViewController : DailyValueScheduleTableViewController {
  22. public init(allowedBasalRates: [Double], maximumScheduleItemCount: Int, minimumTimeInterval: TimeInterval) {
  23. self.allowedBasalRates = allowedBasalRates
  24. self.maximumScheduleItemCount = maximumScheduleItemCount
  25. self.minimumTimeInterval = minimumTimeInterval
  26. super.init(style: .grouped)
  27. }
  28. public required init?(coder aDecoder: NSCoder) {
  29. fatalError("init(coder:) has not been implemented")
  30. }
  31. open override func viewDidLoad() {
  32. super.viewDidLoad()
  33. tableView.register(SetConstrainedScheduleEntryTableViewCell.nib(), forCellReuseIdentifier: SetConstrainedScheduleEntryTableViewCell.className)
  34. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  35. updateEditButton()
  36. }
  37. open override func viewWillDisappear(_ animated: Bool) {
  38. super.viewWillDisappear(animated)
  39. if syncSource == nil {
  40. delegate?.dailyValueScheduleTableViewControllerWillFinishUpdating(self)
  41. }
  42. }
  43. @objc private func cancel(_ sender: Any?) {
  44. self.navigationController?.popViewController(animated: true)
  45. }
  46. // MARK: - State
  47. public var scheduleItems: [RepeatingScheduleValue<Double>] = [] {
  48. didSet {
  49. updateInsertButton()
  50. }
  51. }
  52. let allowedBasalRates: [Double]
  53. let maximumScheduleItemCount: Int
  54. let minimumTimeInterval: TimeInterval
  55. var lastValidStartTime: TimeInterval {
  56. return TimeInterval.hours(24) - minimumTimeInterval
  57. }
  58. private var isScheduleModified = false {
  59. didSet {
  60. if isScheduleModified && syncSource != nil {
  61. self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(_:)))
  62. } else {
  63. self.navigationItem.leftBarButtonItem = nil
  64. }
  65. }
  66. }
  67. private func isBasalRateValid(_ value: Double) -> Bool {
  68. return allowedBasalRates.contains(value)
  69. }
  70. private var isSyncAllowed: Bool {
  71. return !isSyncInProgress && isScheduleValid && !isEditing
  72. }
  73. private var isCellReadOnly: Bool {
  74. return isReadOnly || isSyncInProgress
  75. }
  76. private func updateSyncButton() {
  77. if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: Section.sync.rawValue)) as? TextButtonTableViewCell {
  78. cell.isEnabled = isSyncAllowed
  79. }
  80. }
  81. private func updateEditButton() {
  82. editButtonItem.isEnabled = scheduleItems.endIndex > 1
  83. }
  84. private func updateInsertButton() {
  85. guard let lastItem = scheduleItems.last else {
  86. return
  87. }
  88. insertButtonItem.isEnabled = scheduleItems.endIndex < maximumScheduleItemCount && !isEditing && lastItem.startTime < lastValidStartTime
  89. }
  90. override func addScheduleItem(_ sender: Any?) {
  91. guard !isReadOnly && !isSyncInProgress, let firstBasalRate = allowedBasalRates.first else {
  92. return
  93. }
  94. tableView.endEditing(false)
  95. let startTime: TimeInterval
  96. let value: Double
  97. if let lastItem = scheduleItems.last {
  98. startTime = lastItem.startTime + minimumTimeInterval
  99. value = lastItem.value
  100. if startTime > lastValidStartTime {
  101. return
  102. }
  103. } else {
  104. startTime = TimeInterval(0)
  105. value = firstBasalRate
  106. }
  107. scheduleItems.append(
  108. RepeatingScheduleValue(
  109. startTime: startTime,
  110. value: value
  111. )
  112. )
  113. isScheduleModified = true
  114. updateTimeLimitsForItemsAdjacent(to: scheduleItems.endIndex-1)
  115. super.addScheduleItem(sender)
  116. updateSyncButton()
  117. updateEditButton()
  118. }
  119. override func insertableIndiciesByRemovingRow(_ row: Int, withInterval timeInterval: TimeInterval) -> [Bool] {
  120. return insertableIndices(for: scheduleItems, removing: row, with: timeInterval)
  121. }
  122. open override func setEditing(_ editing: Bool, animated: Bool) {
  123. tableView.beginUpdates()
  124. hideSetConstrainedScheduleEntryCells()
  125. tableView.endUpdates()
  126. super.setEditing(editing, animated: animated)
  127. updateInsertButton()
  128. updateSyncButton()
  129. }
  130. public weak var syncSource: BasalScheduleTableViewControllerSyncSource? {
  131. didSet {
  132. isReadOnly = syncSource?.basalScheduleTableViewControllerIsReadOnly(self) ?? false
  133. if isViewLoaded {
  134. tableView.reloadData()
  135. }
  136. }
  137. }
  138. private var isSyncInProgress = false {
  139. didSet {
  140. for cell in tableView.visibleCells {
  141. switch cell {
  142. case let cell as TextButtonTableViewCell:
  143. cell.isEnabled = !isSyncInProgress
  144. cell.isLoading = isSyncInProgress
  145. case let cell as SetConstrainedScheduleEntryTableViewCell:
  146. cell.isReadOnly = isCellReadOnly
  147. default:
  148. break
  149. }
  150. }
  151. for item in navigationItem.rightBarButtonItems ?? [] {
  152. item.isEnabled = !isSyncInProgress
  153. }
  154. navigationItem.hidesBackButton = isSyncInProgress
  155. }
  156. }
  157. public var isScheduleValid: Bool {
  158. return !scheduleItems.isEmpty &&
  159. scheduleItems.count <= maximumScheduleItemCount &&
  160. scheduleItems.allSatisfy { isBasalRateValid($0.value) }
  161. }
  162. private func updateTimeLimitsFor(itemAt index: Int) {
  163. guard scheduleItems.indices.contains(index) else {
  164. return
  165. }
  166. let indexPath = IndexPath(row: index, section: Section.schedule.rawValue)
  167. if let cell = tableView.cellForRow(at: indexPath) as? SetConstrainedScheduleEntryTableViewCell {
  168. if index+1 < scheduleItems.endIndex {
  169. cell.maximumStartTime = scheduleItems[index+1].startTime - minimumTimeInterval
  170. } else {
  171. cell.maximumStartTime = lastValidStartTime
  172. }
  173. if index > 1 {
  174. cell.minimumStartTime = scheduleItems[index-1].startTime + minimumTimeInterval
  175. }
  176. }
  177. }
  178. private func updateTimeLimitsForItemsAdjacent(to index: Int) {
  179. updateTimeLimitsFor(itemAt: index-1)
  180. updateTimeLimitsFor(itemAt: index+1)
  181. }
  182. // MARK: - UITableViewDataSource
  183. private enum Section: Int {
  184. case schedule
  185. case sync
  186. }
  187. open override func numberOfSections(in tableView: UITableView) -> Int {
  188. if syncSource != nil {
  189. return 2
  190. }
  191. return 1
  192. }
  193. open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  194. switch Section(rawValue: section)! {
  195. case .schedule:
  196. return scheduleItems.endIndex
  197. case .sync:
  198. return 1
  199. }
  200. }
  201. open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  202. switch Section(rawValue: indexPath.section)! {
  203. case .schedule:
  204. let cell = tableView.dequeueReusableCell(withIdentifier: SetConstrainedScheduleEntryTableViewCell.className, for: indexPath) as! SetConstrainedScheduleEntryTableViewCell
  205. cell.unit = HKUnit.internationalUnitsPerHour
  206. cell.valueQuantityFormatter.numberFormatter.maximumFractionDigits = 3
  207. let item = scheduleItems[indexPath.row]
  208. cell.allowedValues = allowedBasalRates
  209. cell.minimumTimeInterval = minimumTimeInterval
  210. cell.isReadOnly = isCellReadOnly
  211. cell.isPickerHidden = true
  212. cell.delegate = self
  213. cell.timeZone = timeZone
  214. if indexPath.row > 0 {
  215. let lastItem = scheduleItems[indexPath.row - 1]
  216. cell.minimumStartTime = lastItem.startTime + minimumTimeInterval
  217. }
  218. if indexPath.row == 0 {
  219. cell.maximumStartTime = 0
  220. } else if indexPath.row < scheduleItems.endIndex - 1 {
  221. let nextItem = scheduleItems[indexPath.row + 1]
  222. cell.maximumStartTime = nextItem.startTime - minimumTimeInterval
  223. } else {
  224. cell.maximumStartTime = lastValidStartTime
  225. }
  226. cell.value = item.value
  227. cell.startTime = item.startTime
  228. return cell
  229. case .sync:
  230. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  231. cell.textLabel?.text = syncSource?.syncButtonTitle(for: self)
  232. cell.isEnabled = isSyncAllowed
  233. cell.isLoading = isSyncInProgress
  234. return cell
  235. }
  236. }
  237. open override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  238. switch Section(rawValue: section)! {
  239. case .schedule:
  240. return nil
  241. case .sync:
  242. return syncSource?.syncButtonDetailText(for: self)
  243. }
  244. }
  245. open override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  246. if editingStyle == .delete {
  247. scheduleItems.remove(at: indexPath.row)
  248. super.tableView(tableView, commit: editingStyle, forRowAt: indexPath)
  249. if scheduleItems.count == 1 {
  250. self.isEditing = false
  251. }
  252. updateSyncButton()
  253. updateInsertButton()
  254. updateEditButton()
  255. updateTimeLimitsFor(itemAt: indexPath.row-1)
  256. updateTimeLimitsFor(itemAt: indexPath.row)
  257. isScheduleModified = true
  258. }
  259. }
  260. open override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  261. if sourceIndexPath != destinationIndexPath {
  262. let item = scheduleItems.remove(at: sourceIndexPath.row)
  263. scheduleItems.insert(item, at: destinationIndexPath.row)
  264. isScheduleModified = true
  265. guard destinationIndexPath.row > 0, let cell = tableView.cellForRow(at: destinationIndexPath) as? SetConstrainedScheduleEntryTableViewCell else {
  266. return
  267. }
  268. let interval = cell.minimumTimeInterval
  269. let startTime = scheduleItems[destinationIndexPath.row - 1].startTime + interval
  270. scheduleItems[destinationIndexPath.row] = RepeatingScheduleValue(startTime: startTime, value: scheduleItems[destinationIndexPath.row].value)
  271. DispatchQueue.main.async {
  272. tableView.reloadRows(at: [destinationIndexPath], with: .none)
  273. self.updateTimeLimitsForItemsAdjacent(to: destinationIndexPath.row)
  274. }
  275. }
  276. }
  277. // MARK: - UITableViewDelegate
  278. open override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  279. guard indexPath.section == 0 else {
  280. return super.tableView(tableView, shouldHighlightRowAt: indexPath)
  281. }
  282. return !isReadOnly
  283. }
  284. open override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  285. tableView.beginUpdates()
  286. hideSetConstrainedScheduleEntryCells(excluding: indexPath)
  287. tableView.endUpdates()
  288. return super.tableView(tableView, willSelectRowAt: indexPath)
  289. }
  290. open override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  291. return super.tableView(tableView, canEditRowAt: indexPath) && !isSyncInProgress
  292. }
  293. open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  294. super.tableView(tableView, didSelectRowAt: indexPath)
  295. switch Section(rawValue: indexPath.section)! {
  296. case .schedule:
  297. break
  298. case .sync:
  299. if let syncSource = syncSource, !isSyncInProgress {
  300. isSyncInProgress = true
  301. syncSource.syncScheduleValues(for: self) { (result) in
  302. DispatchQueue.main.async {
  303. switch result {
  304. case .success(let items, let timeZone):
  305. self.scheduleItems = items
  306. self.timeZone = timeZone
  307. self.tableView.reloadSections([Section.schedule.rawValue], with: .fade)
  308. self.isSyncInProgress = false
  309. self.delegate?.dailyValueScheduleTableViewControllerWillFinishUpdating(self)
  310. self.isScheduleModified = false
  311. self.updateInsertButton()
  312. case .failure(let error):
  313. self.present(UIAlertController(with: error), animated: true) {
  314. self.isSyncInProgress = false
  315. }
  316. }
  317. }
  318. }
  319. }
  320. }
  321. }
  322. open override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
  323. guard sourceIndexPath != proposedDestinationIndexPath, let cell = tableView.cellForRow(at: sourceIndexPath) as? SetConstrainedScheduleEntryTableViewCell else {
  324. return proposedDestinationIndexPath
  325. }
  326. let interval = cell.minimumTimeInterval
  327. let indices = insertableIndices(for: scheduleItems, removing: sourceIndexPath.row, with: interval)
  328. let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
  329. return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
  330. }
  331. }
  332. extension BasalScheduleTableViewController: SetConstrainedScheduleEntryTableViewCellDelegate {
  333. func setConstrainedScheduleEntryTableViewCellDidUpdate(_ cell: SetConstrainedScheduleEntryTableViewCell) {
  334. guard let value = cell.value else {
  335. return
  336. }
  337. if let indexPath = tableView.indexPath(for: cell) {
  338. isScheduleModified = true
  339. scheduleItems[indexPath.row] = RepeatingScheduleValue(
  340. startTime: cell.startTime,
  341. value: value
  342. )
  343. updateTimeLimitsForItemsAdjacent(to: indexPath.row)
  344. updateSyncButton()
  345. }
  346. }
  347. }