SingleValueScheduleTableViewController.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. //
  2. // SingleValueScheduleTableViewController.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 LoopKit
  10. public enum RepeatingScheduleValueResult<T: RawRepresentable> {
  11. case success(scheduleItems: [RepeatingScheduleValue<T>], timeZone: TimeZone)
  12. case failure(Error)
  13. }
  14. public protocol SingleValueScheduleTableViewControllerSyncSource: AnyObject {
  15. func syncScheduleValues(for viewController: SingleValueScheduleTableViewController, completion: @escaping (_ result: RepeatingScheduleValueResult<Double>) -> Void)
  16. func syncButtonTitle(for viewController: SingleValueScheduleTableViewController) -> String
  17. func syncButtonDetailText(for viewController: SingleValueScheduleTableViewController) -> String?
  18. func singleValueScheduleTableViewControllerIsReadOnly(_ viewController: SingleValueScheduleTableViewController) -> Bool
  19. }
  20. open class SingleValueScheduleTableViewController: DailyValueScheduleTableViewController, RepeatingScheduleValueTableViewCellDelegate {
  21. open override func viewDidLoad() {
  22. super.viewDidLoad()
  23. tableView.register(RepeatingScheduleValueTableViewCell.nib(), forCellReuseIdentifier: RepeatingScheduleValueTableViewCell.className)
  24. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  25. }
  26. open override func viewWillDisappear(_ animated: Bool) {
  27. super.viewWillDisappear(animated)
  28. if syncSource == nil {
  29. delegate?.dailyValueScheduleTableViewControllerWillFinishUpdating(self)
  30. }
  31. }
  32. // MARK: - State
  33. public var scheduleItems: [RepeatingScheduleValue<Double>] = []
  34. override func addScheduleItem(_ sender: Any?) {
  35. guard !isReadOnly && !isSyncInProgress else {
  36. return
  37. }
  38. tableView.endEditing(false)
  39. var startTime = TimeInterval(0)
  40. var value = 0.0
  41. if scheduleItems.count > 0, let cell = tableView.cellForRow(at: IndexPath(row: scheduleItems.count - 1, section: 0)) as? RepeatingScheduleValueTableViewCell {
  42. let lastItem = scheduleItems.last!
  43. let interval = cell.datePickerInterval
  44. startTime = lastItem.startTime + interval
  45. value = lastItem.value
  46. if startTime >= TimeInterval(hours: 24) {
  47. return
  48. }
  49. }
  50. scheduleItems.append(
  51. RepeatingScheduleValue(
  52. startTime: min(TimeInterval(hours: 23.5), startTime),
  53. value: value
  54. )
  55. )
  56. super.addScheduleItem(sender)
  57. }
  58. override func insertableIndiciesByRemovingRow(_ row: Int, withInterval timeInterval: TimeInterval) -> [Bool] {
  59. return insertableIndices(for: scheduleItems, removing: row, with: timeInterval)
  60. }
  61. var preferredValueFractionDigits: Int {
  62. return 1
  63. }
  64. public weak var syncSource: SingleValueScheduleTableViewControllerSyncSource? {
  65. didSet {
  66. isReadOnly = syncSource?.singleValueScheduleTableViewControllerIsReadOnly(self) ?? false
  67. if isViewLoaded {
  68. tableView.reloadData()
  69. }
  70. }
  71. }
  72. private var isSyncInProgress = false {
  73. didSet {
  74. for cell in tableView.visibleCells {
  75. switch cell {
  76. case let cell as TextButtonTableViewCell:
  77. cell.isEnabled = !isSyncInProgress
  78. cell.isLoading = isSyncInProgress
  79. case let cell as RepeatingScheduleValueTableViewCell:
  80. cell.isReadOnly = isReadOnly || isSyncInProgress
  81. default:
  82. break
  83. }
  84. }
  85. for item in navigationItem.rightBarButtonItems ?? [] {
  86. item.isEnabled = !isSyncInProgress
  87. }
  88. navigationItem.hidesBackButton = isSyncInProgress
  89. }
  90. }
  91. // MARK: - UITableViewDataSource
  92. private enum Section: Int {
  93. case schedule
  94. case sync
  95. }
  96. open override func numberOfSections(in tableView: UITableView) -> Int {
  97. if syncSource != nil {
  98. return 2
  99. }
  100. return 1
  101. }
  102. open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  103. switch Section(rawValue: section)! {
  104. case .schedule:
  105. return scheduleItems.count
  106. case .sync:
  107. return 1
  108. }
  109. }
  110. open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  111. switch Section(rawValue: indexPath.section)! {
  112. case .schedule:
  113. let cell = tableView.dequeueReusableCell(withIdentifier: RepeatingScheduleValueTableViewCell.className, for: indexPath) as! RepeatingScheduleValueTableViewCell
  114. let item = scheduleItems[indexPath.row]
  115. let interval = cell.datePickerInterval
  116. cell.timeZone = timeZone
  117. cell.date = midnight.addingTimeInterval(item.startTime)
  118. cell.valueNumberFormatter.minimumFractionDigits = preferredValueFractionDigits
  119. cell.value = item.value
  120. cell.unitString = unitDisplayString
  121. cell.isReadOnly = isReadOnly || isSyncInProgress
  122. cell.delegate = self
  123. if indexPath.row > 0 {
  124. let lastItem = scheduleItems[indexPath.row - 1]
  125. cell.datePicker.minimumDate = midnight.addingTimeInterval(lastItem.startTime).addingTimeInterval(interval)
  126. }
  127. if indexPath.row < scheduleItems.endIndex - 1 {
  128. let nextItem = scheduleItems[indexPath.row + 1]
  129. cell.datePicker.maximumDate = midnight.addingTimeInterval(nextItem.startTime).addingTimeInterval(-interval)
  130. } else {
  131. cell.datePicker.maximumDate = midnight.addingTimeInterval(TimeInterval(hours: 24) - interval)
  132. }
  133. return cell
  134. case .sync:
  135. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  136. cell.textLabel?.text = syncSource?.syncButtonTitle(for: self)
  137. cell.isEnabled = !isSyncInProgress
  138. cell.isLoading = isSyncInProgress
  139. return cell
  140. }
  141. }
  142. open override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  143. switch Section(rawValue: section)! {
  144. case .schedule:
  145. return nil
  146. case .sync:
  147. return syncSource?.syncButtonDetailText(for: self)
  148. }
  149. }
  150. open override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  151. if editingStyle == .delete {
  152. scheduleItems.remove(at: indexPath.row)
  153. super.tableView(tableView, commit: editingStyle, forRowAt: indexPath)
  154. }
  155. }
  156. open override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  157. if sourceIndexPath != destinationIndexPath {
  158. let item = scheduleItems.remove(at: sourceIndexPath.row)
  159. scheduleItems.insert(item, at: destinationIndexPath.row)
  160. guard destinationIndexPath.row > 0, let cell = tableView.cellForRow(at: destinationIndexPath) as? RepeatingScheduleValueTableViewCell else {
  161. return
  162. }
  163. let interval = cell.datePickerInterval
  164. let startTime = scheduleItems[destinationIndexPath.row - 1].startTime + interval
  165. scheduleItems[destinationIndexPath.row] = RepeatingScheduleValue(startTime: startTime, value: scheduleItems[destinationIndexPath.row].value)
  166. // Since the valid date ranges of neighboring cells are affected, the lazy solution is to just reload the entire table view
  167. DispatchQueue.main.async {
  168. tableView.reloadData()
  169. }
  170. }
  171. }
  172. // MARK: - UITableViewDelegate
  173. open override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  174. return super.tableView(tableView, canEditRowAt: indexPath) && !isSyncInProgress
  175. }
  176. open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  177. super.tableView(tableView, didSelectRowAt: indexPath)
  178. switch Section(rawValue: indexPath.section)! {
  179. case .schedule:
  180. break
  181. case .sync:
  182. if let syncSource = syncSource, !isSyncInProgress {
  183. isSyncInProgress = true
  184. syncSource.syncScheduleValues(for: self) { (result) in
  185. DispatchQueue.main.async {
  186. switch result {
  187. case .success(let items, let timeZone):
  188. self.scheduleItems = items
  189. self.timeZone = timeZone
  190. self.tableView.reloadSections([Section.schedule.rawValue], with: .fade)
  191. self.isSyncInProgress = false
  192. self.delegate?.dailyValueScheduleTableViewControllerWillFinishUpdating(self)
  193. case .failure(let error):
  194. self.present(UIAlertController(with: error), animated: true) {
  195. self.isSyncInProgress = false
  196. }
  197. }
  198. }
  199. }
  200. }
  201. }
  202. }
  203. open override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
  204. guard sourceIndexPath != proposedDestinationIndexPath, let cell = tableView.cellForRow(at: sourceIndexPath) as? RepeatingScheduleValueTableViewCell else {
  205. return proposedDestinationIndexPath
  206. }
  207. let interval = cell.datePickerInterval
  208. let indices = insertableIndices(for: scheduleItems, removing: sourceIndexPath.row, with: interval)
  209. let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
  210. return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
  211. }
  212. // MARK: - RepeatingScheduleValueTableViewCellDelegate
  213. override public func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
  214. if let indexPath = tableView.indexPath(for: cell) {
  215. let currentItem = scheduleItems[indexPath.row]
  216. scheduleItems[indexPath.row] = RepeatingScheduleValue(
  217. startTime: cell.date.timeIntervalSince(midnight),
  218. value: currentItem.value
  219. )
  220. }
  221. super.datePickerTableViewCellDidUpdateDate(cell)
  222. }
  223. func repeatingScheduleValueTableViewCellDidUpdateValue(_ cell: RepeatingScheduleValueTableViewCell) {
  224. if let indexPath = tableView.indexPath(for: cell) {
  225. let currentItem = scheduleItems[indexPath.row]
  226. scheduleItems[indexPath.row] = RepeatingScheduleValue(startTime: currentItem.startTime, value: cell.value)
  227. }
  228. }
  229. }