SetConstrainedScheduleEntryTableViewCell.swift 9.2 KB

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