SetConstrainedScheduleEntryTableViewCell.swift 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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.setPreferredNumberFormatter(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. lazy var valueQuantityFormatter: QuantityFormatter = {
  137. let formatter = QuantityFormatter()
  138. return formatter
  139. }()
  140. private func startTimeForTimeComponent(row: Int) -> TimeInterval {
  141. return minimumStartTime + minimumTimeInterval * TimeInterval(row)
  142. }
  143. private func stringForStartTime(_ time: TimeInterval) -> String {
  144. let date = startOfDay.addingTimeInterval(time)
  145. return dateFormatter.string(from: date)
  146. }
  147. func updateDateLabel() {
  148. dateLabel.text = stringForStartTime(startTime)
  149. }
  150. func validate() {
  151. if let value = value, allowedValues.contains(value) {
  152. valueLabel.textColor = nil // Default color
  153. } else {
  154. valueLabel.textColor = .systemRed
  155. }
  156. }
  157. func updateValueFromPicker() {
  158. let index = picker.selectedRow(inComponent: Component.value.rawValue) - emptySelectionType.rowOffset
  159. if index >= 0 && index < allowedValues.count {
  160. value = allowedValues[index]
  161. } else {
  162. value = nil
  163. }
  164. updateValueLabel()
  165. }
  166. private func updateStartTimeSelection() {
  167. let row = Int(round((startTime - minimumStartTime) / minimumTimeInterval))
  168. if row >= 0 && row < pickerView(picker, numberOfRowsInComponent: Component.time.rawValue) {
  169. picker.selectRow(row, inComponent: Component.time.rawValue, animated: true)
  170. }
  171. }
  172. func updateValuePicker() {
  173. guard !allowedValues.isEmpty else {
  174. return
  175. }
  176. let selectedIndex: Int
  177. if let value = value {
  178. if let row = allowedValues.firstIndex(of: value) {
  179. selectedIndex = row + emptySelectionType.rowOffset
  180. } else {
  181. // Select next highest value
  182. selectedIndex = (allowedValues.enumerated().filter({$0.element >= value}).min(by: { $0.1 < $1.1 })?.offset ?? 0) + emptySelectionType.rowOffset
  183. }
  184. } else {
  185. switch emptySelectionType {
  186. case .none:
  187. selectedIndex = allowedValues.count - 1
  188. case .firstIndex:
  189. selectedIndex = 0
  190. case .lastIndex:
  191. selectedIndex = allowedValues.count
  192. }
  193. }
  194. picker.selectRow(selectedIndex, inComponent: Component.value.rawValue, animated: true)
  195. }
  196. func updateValueLabel() {
  197. guard let value = value else {
  198. valueLabel.text = nil
  199. return
  200. }
  201. validate()
  202. valueLabel.text = formatValue(value)
  203. }
  204. private func formatValue(_ value: Double) -> String? {
  205. if let unit = unit {
  206. let quantity = HKQuantity(unit: unit, doubleValue: value)
  207. return valueQuantityFormatter.string(from: quantity, for: unit)
  208. } else {
  209. return valueQuantityFormatter.numberFormatter.string(from: value)
  210. }
  211. }
  212. }
  213. extension SetConstrainedScheduleEntryTableViewCell: UIPickerViewDelegate {
  214. func pickerView(_ pickerView: UIPickerView,
  215. didSelectRow row: Int,
  216. inComponent component: Int) {
  217. switch Component(rawValue: component)! {
  218. case .time:
  219. startTime = selectedStartTime
  220. case .value:
  221. updateValueFromPicker()
  222. }
  223. delegate?.setConstrainedScheduleEntryTableViewCellDidUpdate(self)
  224. }
  225. func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
  226. let metrics = UIFontMetrics(forTextStyle: .body)
  227. return metrics.scaledValue(for: 32)
  228. }
  229. func pickerView(_ pickerView: UIPickerView,
  230. titleForRow row: Int,
  231. forComponent component: Int) -> String? {
  232. switch Component(rawValue: component)! {
  233. case .time:
  234. let time = startTimeForTimeComponent(row: row)
  235. return stringForStartTime(time)
  236. case .value:
  237. let valueRow = row - emptySelectionType.rowOffset
  238. guard valueRow >= 0 && valueRow < allowedValues.count else {
  239. return nil
  240. }
  241. return formatValue(allowedValues[valueRow])
  242. }
  243. }
  244. }
  245. extension SetConstrainedScheduleEntryTableViewCell: UIPickerViewDataSource {
  246. func numberOfComponents(in pickerView: UIPickerView) -> Int {
  247. return Component.allCases.count
  248. }
  249. func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
  250. switch Component(rawValue: component)! {
  251. case .time:
  252. return Int(round((maximumStartTime - minimumStartTime) / minimumTimeInterval) + 1)
  253. case .value:
  254. return allowedValues.count + emptySelectionType.rowCount
  255. }
  256. }
  257. }
  258. /// UITableViewController extensions to aid working with DatePickerTableViewCell
  259. extension SetConstrainedScheduleEntryTableViewCellDelegate where Self: UITableViewController {
  260. func hideSetConstrainedScheduleEntryCells(excluding indexPath: IndexPath? = nil) {
  261. for case let cell as SetConstrainedScheduleEntryTableViewCell in tableView.visibleCells where tableView.indexPath(for: cell) != indexPath && cell.isPickerHidden == false {
  262. cell.isPickerHidden = true
  263. }
  264. }
  265. }