DailyValueScheduleTableViewController.swift 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. //
  2. // DailyValueScheduleTableViewController.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 2/6/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import LoopKit
  10. public protocol DailyValueScheduleTableViewControllerDelegate: AnyObject {
  11. func dailyValueScheduleTableViewControllerWillFinishUpdating(_ controller: DailyValueScheduleTableViewController)
  12. }
  13. func insertableIndices<T>(for scheduleItems: [RepeatingScheduleValue<T>], removing row: Int, with interval: TimeInterval) -> [Bool] {
  14. let insertableIndices = scheduleItems.enumerated().map { (enumeration) -> Bool in
  15. let (index, item) = enumeration
  16. if row == index {
  17. return true
  18. } else if index == 0 {
  19. return false
  20. } else if index == scheduleItems.endIndex - 1 {
  21. return item.startTime < TimeInterval(hours: 24) - interval
  22. } else if index > row {
  23. return scheduleItems[index + 1].startTime - item.startTime > interval
  24. } else {
  25. return item.startTime - scheduleItems[index - 1].startTime > interval
  26. }
  27. }
  28. return insertableIndices
  29. }
  30. open class DailyValueScheduleTableViewController: UITableViewController, DatePickerTableViewCellDelegate {
  31. private var keyboardWillShowNotificationObserver: Any?
  32. public convenience init() {
  33. self.init(style: .plain)
  34. }
  35. open override func viewDidLoad() {
  36. super.viewDidLoad()
  37. tableView.rowHeight = UITableView.automaticDimension
  38. tableView.estimatedRowHeight = 44
  39. if !isReadOnly {
  40. navigationItem.rightBarButtonItems = [insertButtonItem, editButtonItem]
  41. }
  42. tableView.keyboardDismissMode = .onDrag
  43. keyboardWillShowNotificationObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (note) -> Void in
  44. guard let strongSelf = self else {
  45. return
  46. }
  47. guard note.userInfo?[UIResponder.keyboardIsLocalUserInfoKey] as? Bool == true else {
  48. return
  49. }
  50. let animated = note.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0 > 0
  51. if let indexPath = strongSelf.tableView.indexPathForSelectedRow {
  52. strongSelf.tableView.beginUpdates()
  53. strongSelf.tableView.deselectRow(at: indexPath, animated: animated)
  54. strongSelf.tableView.endUpdates()
  55. }
  56. })
  57. }
  58. open override func setEditing(_ editing: Bool, animated: Bool) {
  59. if let indexPath = tableView.indexPathForSelectedRow {
  60. tableView.beginUpdates()
  61. tableView.deselectRow(at: indexPath, animated: animated)
  62. tableView.endUpdates()
  63. }
  64. tableView.endEditing(false)
  65. navigationItem.rightBarButtonItems?[0].isEnabled = !editing
  66. super.setEditing(editing, animated: animated)
  67. }
  68. deinit {
  69. if let observer = keyboardWillShowNotificationObserver {
  70. NotificationCenter.default.removeObserver(observer)
  71. }
  72. }
  73. open override func viewWillDisappear(_ animated: Bool) {
  74. super.viewWillDisappear(animated)
  75. tableView.endEditing(true)
  76. }
  77. public weak var delegate: DailyValueScheduleTableViewControllerDelegate?
  78. public private(set) lazy var insertButtonItem: UIBarButtonItem = {
  79. return UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addScheduleItem(_:)))
  80. }()
  81. // MARK: - State
  82. public var timeZone = TimeZone.currentFixed {
  83. didSet {
  84. calendar.timeZone = timeZone
  85. let localTimeZone = TimeZone.current
  86. let timeZoneDiff = TimeInterval(timeZone.secondsFromGMT() - localTimeZone.secondsFromGMT())
  87. if timeZoneDiff != 0 {
  88. let localTimeZoneName = localTimeZone.abbreviation() ?? localTimeZone.identifier
  89. let formatter = DateComponentsFormatter()
  90. formatter.allowedUnits = [.hour, .minute]
  91. let diffString = formatter.string(from: abs(timeZoneDiff)) ?? String(abs(timeZoneDiff))
  92. navigationItem.prompt = String(
  93. format: LocalizedString("Times in %1$@%2$@%3$@", comment: "The schedule table view header describing the configured time zone difference from the default time zone. The substitution parameters are: (1: time zone name)(2: +/-)(3: time interval)"),
  94. localTimeZoneName, timeZoneDiff < 0 ? "-" : "+", diffString
  95. )
  96. }
  97. }
  98. }
  99. public var unitDisplayString: String = "U/hour"
  100. public var isReadOnly: Bool = false {
  101. didSet {
  102. if isReadOnly {
  103. isEditing = false
  104. }
  105. if isViewLoaded {
  106. navigationItem.setRightBarButtonItems(isReadOnly ? [] : [insertButtonItem, editButtonItem], animated: true)
  107. }
  108. }
  109. }
  110. private var calendar = Calendar.current
  111. var midnight: Date {
  112. return calendar.startOfDay(for: Date())
  113. }
  114. @objc func addScheduleItem(_ sender: Any?) {
  115. guard !isReadOnly else {
  116. return
  117. }
  118. // Updates the table view state. Subclasses should update their data model before calling super
  119. tableView.insertRows(at: [IndexPath(row: tableView.numberOfRows(inSection: 0), section: 0)], with: .automatic)
  120. }
  121. func insertableIndiciesByRemovingRow(_ row: Int, withInterval timeInterval: TimeInterval) -> [Bool] {
  122. fatalError("Subclasses must override \(#function)")
  123. }
  124. // MARK: - UITableViewDataSource
  125. open override func numberOfSections(in tableView: UITableView) -> Int {
  126. return 1
  127. }
  128. open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  129. fatalError("Subclasses must override \(#function)")
  130. }
  131. open override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  132. return !isReadOnly && indexPath.section == 0 && indexPath.row > 0
  133. }
  134. open override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
  135. return self.tableView(tableView, canEditRowAt: indexPath)
  136. }
  137. open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  138. fatalError("Subclasses must override \(#function)")
  139. }
  140. open override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  141. if editingStyle == .delete {
  142. // Updates the table view state. Subclasses should update their data model before calling super
  143. tableView.deleteRows(at: [indexPath], with: .automatic)
  144. }
  145. }
  146. // MARK: - UITableViewDelegate
  147. open override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  148. guard indexPath.section == 0 else {
  149. return true
  150. }
  151. return !isReadOnly && indexPath.row > 0
  152. }
  153. open override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  154. guard self.tableView(tableView, shouldHighlightRowAt: indexPath) else {
  155. return nil
  156. }
  157. tableView.endEditing(false)
  158. tableView.beginUpdates()
  159. hideDatePickerCells(excluding: indexPath)
  160. return indexPath
  161. }
  162. open override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
  163. tableView.beginUpdates()
  164. tableView.endUpdates()
  165. }
  166. open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  167. tableView.endEditing(false)
  168. tableView.endUpdates()
  169. tableView.deselectRow(at: indexPath, animated: true)
  170. }
  171. open override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
  172. guard sourceIndexPath.section == proposedDestinationIndexPath.section else {
  173. return sourceIndexPath
  174. }
  175. guard sourceIndexPath != proposedDestinationIndexPath, let cell = tableView.cellForRow(at: sourceIndexPath) as? RepeatingScheduleValueTableViewCell else {
  176. return proposedDestinationIndexPath
  177. }
  178. let interval = cell.datePickerInterval
  179. let indices = insertableIndiciesByRemovingRow(sourceIndexPath.row, withInterval: interval)
  180. let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
  181. return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
  182. }
  183. // MARK: - DatePickerTableViewCellDelegate
  184. public func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
  185. // Updates the TableView state. Subclasses should update their data model
  186. if let indexPath = tableView.indexPath(for: cell) {
  187. var indexPaths: [IndexPath] = []
  188. if indexPath.row > 0 {
  189. indexPaths.append(IndexPath(row: indexPath.row - 1, section: indexPath.section))
  190. }
  191. if indexPath.row < tableView.numberOfRows(inSection: 0) - 1 {
  192. indexPaths.append(IndexPath(row: indexPath.row + 1, section: indexPath.section))
  193. }
  194. DispatchQueue.main.async {
  195. self.tableView.reloadRows(at: indexPaths, with: .none)
  196. }
  197. }
  198. }
  199. }
  200. extension Array where Element == Bool {
  201. func insertableIndex(closestTo destination: Int, from source: Int) -> Int {
  202. if self[destination] {
  203. return destination
  204. } else {
  205. var closestRow = source
  206. for (index, valid) in self.enumerated() where valid {
  207. if abs(destination - index) < abs(destination - closestRow) {
  208. closestRow = index
  209. }
  210. }
  211. return closestRow
  212. }
  213. }
  214. }