SetConstrainedScheduleEntryTableViewCell.swift 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. //
  2. // SetConstrainedScheduleEntryTableViewCell.swift
  3. // LoopKitUI
  4. //
  5. // Created by Pete Schwamb on 2/23/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. import LoopKit
  11. protocol SetConstrainedScheduleEntryTableViewCellDelegate: class {
  12. func setConstrainedScheduleEntryTableViewCellDidUpdate(_ cell: SetConstrainedScheduleEntryTableViewCell)
  13. }
  14. private enum Component: Int, CaseIterable {
  15. case time = 0
  16. case value
  17. }
  18. class SetConstrainedScheduleEntryTableViewCell: UITableViewCell {
  19. public enum EmptySelectionType {
  20. case none
  21. case firstIndex
  22. case lastIndex
  23. var rowCount: Int {
  24. if self == .none {
  25. return 0
  26. } else {
  27. return 1
  28. }
  29. }
  30. var rowOffset: Int {
  31. if self == .firstIndex {
  32. return 1
  33. } else {
  34. return 0
  35. }
  36. }
  37. }
  38. @IBOutlet private weak var picker: UIPickerView!
  39. @IBOutlet private weak var pickerHeightConstraint: NSLayoutConstraint!
  40. private var pickerExpandedHeight: CGFloat = 0
  41. @IBOutlet private weak var dateLabel: UILabel!
  42. @IBOutlet private weak var valueLabel: UILabel!
  43. public weak var delegate: SetConstrainedScheduleEntryTableViewCellDelegate?
  44. public var allowedValues: [Double] = [] {
  45. didSet {
  46. picker.reloadAllComponents()
  47. updateValuePicker()
  48. }
  49. }
  50. public var emptySelectionType = EmptySelectionType.none {
  51. didSet {
  52. picker.reloadAllComponents()
  53. updateValuePicker()
  54. }
  55. }
  56. public var unit: HKUnit? {
  57. didSet {
  58. if let unit = unit {
  59. valueQuantityFormatter.setPreferredNumberFormatter(for: unit)
  60. picker.reloadAllComponents()
  61. updateValuePicker()
  62. }
  63. }
  64. }
  65. public var minimumTimeInterval: TimeInterval = .hours(0.5)
  66. public var minimumStartTime: TimeInterval = .hours(0) {
  67. didSet {
  68. picker.reloadComponent(Component.time.rawValue)
  69. updateStartTimeSelection()
  70. }
  71. }
  72. public var maximumStartTime: TimeInterval = .hours(23.5) {
  73. didSet {
  74. picker.reloadComponent(Component.time.rawValue)
  75. }
  76. }
  77. public var timeZone: TimeZone! {
  78. didSet {
  79. dateFormatter.timeZone = timeZone
  80. var calendar = Calendar.current
  81. calendar.timeZone = timeZone
  82. startOfDay = calendar.startOfDay(for: Date())
  83. }
  84. }
  85. private lazy var startOfDay = Calendar.current.startOfDay(for: Date())
  86. var startTime: TimeInterval = 0 {
  87. didSet {
  88. updateStartTimeSelection()
  89. updateDateLabel()
  90. }
  91. }
  92. var selectedStartTime: TimeInterval {
  93. let row = picker.selectedRow(inComponent: Component.time.rawValue)
  94. return startTimeForTimeComponent(row: row)
  95. }
  96. var value: Double? = nil {
  97. didSet {
  98. updateValuePicker()
  99. updateValueLabel()
  100. }
  101. }
  102. var isPickerHidden: Bool {
  103. get {
  104. return picker.isHidden
  105. }
  106. set {
  107. picker.isHidden = newValue
  108. pickerHeightConstraint.constant = newValue ? 0 : pickerExpandedHeight
  109. if !newValue {
  110. updateValuePicker()
  111. updateStartTimeSelection()
  112. }
  113. }
  114. }
  115. var isReadOnly = false
  116. override func awakeFromNib() {
  117. super.awakeFromNib()
  118. pickerExpandedHeight = pickerHeightConstraint.constant
  119. valueLabel.text = nil
  120. setSelected(true, animated: false)
  121. updateDateLabel()
  122. }
  123. override func setSelected(_ selected: Bool, animated: Bool) {
  124. super.setSelected(selected, animated: animated)
  125. if selected && !isReadOnly {
  126. isPickerHidden.toggle()
  127. }
  128. }
  129. private lazy var dateFormatter: DateFormatter = {
  130. let dateFormatter = DateFormatter()
  131. dateFormatter.dateStyle = .none
  132. dateFormatter.timeStyle = .short
  133. return dateFormatter
  134. }()
  135. lazy var valueQuantityFormatter: QuantityFormatter = {
  136. let formatter = QuantityFormatter()
  137. return formatter
  138. }()
  139. private func startTimeForTimeComponent(row: Int) -> TimeInterval {
  140. return minimumStartTime + minimumTimeInterval * TimeInterval(row)
  141. }
  142. private func stringForStartTime(_ time: TimeInterval) -> String {
  143. let date = startOfDay.addingTimeInterval(time)
  144. return dateFormatter.string(from: date)
  145. }
  146. func updateDateLabel() {
  147. dateLabel.text = stringForStartTime(startTime)
  148. }
  149. func validate() {
  150. if let value = value, allowedValues.contains(value) {
  151. valueLabel.textColor = nil // Default color
  152. } else {
  153. valueLabel.textColor = .systemRed
  154. }
  155. }
  156. func updateValueFromPicker() {
  157. let index = picker.selectedRow(inComponent: Component.value.rawValue) - emptySelectionType.rowOffset
  158. if index >= 0 && index < allowedValues.count {
  159. value = allowedValues[index]
  160. } else {
  161. value = nil
  162. }
  163. updateValueLabel()
  164. }
  165. private func updateStartTimeSelection() {
  166. let row = Int(round((startTime - minimumStartTime) / minimumTimeInterval))
  167. if row >= 0 && row < pickerView(picker, numberOfRowsInComponent: Component.time.rawValue) {
  168. picker.selectRow(row, inComponent: Component.time.rawValue, animated: true)
  169. }
  170. }
  171. func updateValuePicker() {
  172. guard !allowedValues.isEmpty else {
  173. return
  174. }
  175. let selectedIndex: Int
  176. if let value = value {
  177. if let row = allowedValues.firstIndex(of: value) {
  178. selectedIndex = row + emptySelectionType.rowOffset
  179. } else {
  180. // Select next highest value
  181. selectedIndex = (allowedValues.enumerated().filter({$0.element >= value}).min(by: { $0.1 < $1.1 })?.offset ?? 0) + emptySelectionType.rowOffset
  182. }
  183. } else {
  184. switch emptySelectionType {
  185. case .none:
  186. selectedIndex = allowedValues.count - 1
  187. case .firstIndex:
  188. selectedIndex = 0
  189. case .lastIndex:
  190. selectedIndex = allowedValues.count
  191. }
  192. }
  193. picker.selectRow(selectedIndex, inComponent: Component.value.rawValue, animated: true)
  194. }
  195. func updateValueLabel() {
  196. guard let value = value else {
  197. valueLabel.text = nil
  198. return
  199. }
  200. validate()
  201. valueLabel.text = formatValue(value)
  202. }
  203. private func formatValue(_ value: Double) -> String? {
  204. if let unit = unit {
  205. let quantity = HKQuantity(unit: unit, doubleValue: value)
  206. return valueQuantityFormatter.string(from: quantity, for: unit)
  207. } else {
  208. return valueQuantityFormatter.numberFormatter.string(from: value)
  209. }
  210. }
  211. }
  212. extension SetConstrainedScheduleEntryTableViewCell: UIPickerViewDelegate {
  213. func pickerView(_ pickerView: UIPickerView,
  214. didSelectRow row: Int,
  215. inComponent component: Int) {
  216. switch Component(rawValue: component)! {
  217. case .time:
  218. startTime = selectedStartTime
  219. case .value:
  220. updateValueFromPicker()
  221. }
  222. delegate?.setConstrainedScheduleEntryTableViewCellDidUpdate(self)
  223. }
  224. func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
  225. let metrics = UIFontMetrics(forTextStyle: .body)
  226. return metrics.scaledValue(for: 32)
  227. }
  228. func pickerView(_ pickerView: UIPickerView,
  229. titleForRow row: Int,
  230. forComponent component: Int) -> String? {
  231. switch Component(rawValue: component)! {
  232. case .time:
  233. let time = startTimeForTimeComponent(row: row)
  234. return stringForStartTime(time)
  235. case .value:
  236. let valueRow = row - emptySelectionType.rowOffset
  237. guard valueRow >= 0 && valueRow < allowedValues.count else {
  238. return nil
  239. }
  240. return formatValue(allowedValues[valueRow])
  241. }
  242. }
  243. }
  244. extension SetConstrainedScheduleEntryTableViewCell: UIPickerViewDataSource {
  245. func numberOfComponents(in pickerView: UIPickerView) -> Int {
  246. return Component.allCases.count
  247. }
  248. func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
  249. switch Component(rawValue: component)! {
  250. case .time:
  251. return Int(round((maximumStartTime - minimumStartTime) / minimumTimeInterval) + 1)
  252. case .value:
  253. return allowedValues.count + emptySelectionType.rowCount
  254. }
  255. }
  256. }
  257. /// UITableViewController extensions to aid working with DatePickerTableViewCell
  258. extension SetConstrainedScheduleEntryTableViewCellDelegate where Self: UITableViewController {
  259. func hideSetConstrainedScheduleEntryCells(excluding indexPath: IndexPath? = nil) {
  260. for case let cell as SetConstrainedScheduleEntryTableViewCell in tableView.visibleCells where tableView.indexPath(for: cell) != indexPath && cell.isPickerHidden == false {
  261. cell.isPickerHidden = true
  262. }
  263. }
  264. }