GlucoseRangeTableViewCell.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. //
  2. // GlucoseRangeTableViewCell.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. protocol GlucoseRangeTableViewCellDelegate: AnyObject {
  10. func glucoseRangeTableViewCellDidUpdate(_ cell: GlucoseRangeTableViewCell)
  11. }
  12. class GlucoseRangeTableViewCell: UITableViewCell {
  13. public enum Component: Int, CaseIterable {
  14. case time = 0
  15. case minValue
  16. case separator
  17. case maxValue
  18. case units
  19. var placeholderString: String? {
  20. switch self {
  21. case .minValue:
  22. return LocalizedString("min", comment: "Placeholder for minimum value in glucose range")
  23. case .maxValue:
  24. return LocalizedString("max", comment: "Placeholder for maximum value in glucose range")
  25. default:
  26. return nil
  27. }
  28. }
  29. }
  30. @IBOutlet weak var dateLabel: UILabel!
  31. @IBOutlet open weak var picker: UIPickerView!
  32. @IBOutlet open weak var pickerHeightConstraint: NSLayoutConstraint!
  33. @IBOutlet weak var minValueTextField: UITextField! {
  34. didSet {
  35. // Setting this color in code because the nib isn't being applied correctly
  36. minValueTextField.textColor = .label
  37. }
  38. }
  39. @IBOutlet weak var separatorLabel: UILabel! {
  40. didSet {
  41. // Setting this color in code because the nib isn't being applied correctly
  42. separatorLabel.textColor = .secondaryLabel
  43. }
  44. }
  45. @IBOutlet weak var maxValueTextField: UITextField! {
  46. didSet {
  47. // Setting this color in code because the nib isn't being applied correctly
  48. maxValueTextField.textColor = .label
  49. }
  50. }
  51. @IBOutlet weak var unitLabel: UILabel! {
  52. didSet {
  53. // Setting this color in code because the nib isn't being applied correctly
  54. unitLabel.textColor = .secondaryLabel
  55. }
  56. }
  57. private var pickerExpandedHeight: CGFloat = 0
  58. public var minimumTimeInterval: TimeInterval = .hours(0.5)
  59. public weak var delegate: GlucoseRangeTableViewCellDelegate?
  60. var allowTimeSelection: Bool = true
  61. var minValue: Double? {
  62. didSet {
  63. guard let value = minValue else {
  64. minValueTextField.text = nil
  65. return
  66. }
  67. minValueTextField.text = valueNumberFormatter.string(from: value)
  68. }
  69. }
  70. var maxValue: Double? {
  71. didSet {
  72. guard let value = maxValue else {
  73. maxValueTextField.text = nil
  74. return
  75. }
  76. maxValueTextField.text = valueNumberFormatter.string(from: value)
  77. }
  78. }
  79. public var allowedValues: [Double] = [] {
  80. didSet {
  81. picker.reloadAllComponents()
  82. selectPickerValues()
  83. }
  84. }
  85. private var allowedMaxValues: [Double] {
  86. guard let minValue = minValue else {
  87. return allowedValues
  88. }
  89. return allowedValues.filter( { $0 >= minValue })
  90. }
  91. private var allowedMinValues: [Double] {
  92. guard let maxValue = maxValue else {
  93. return allowedValues
  94. }
  95. return allowedValues.filter( { $0 <= maxValue })
  96. }
  97. var unitString: String? {
  98. get {
  99. return unitLabel.text
  100. }
  101. set {
  102. unitLabel.text = newValue
  103. }
  104. }
  105. lazy var valueNumberFormatter: NumberFormatter = {
  106. let formatter = NumberFormatter()
  107. formatter.numberStyle = .decimal
  108. formatter.minimumFractionDigits = 1
  109. return formatter
  110. }()
  111. private lazy var dateFormatter: DateFormatter = {
  112. let dateFormatter = DateFormatter()
  113. dateFormatter.dateStyle = .none
  114. dateFormatter.timeStyle = .short
  115. return dateFormatter
  116. }()
  117. public var timeZone: TimeZone! {
  118. didSet {
  119. dateFormatter.timeZone = timeZone
  120. var calendar = Calendar.current
  121. calendar.timeZone = timeZone
  122. startOfDay = calendar.startOfDay(for: Date())
  123. }
  124. }
  125. private lazy var startOfDay = Calendar.current.startOfDay(for: Date())
  126. var startTime: TimeInterval = 0 {
  127. didSet {
  128. updateStartTimeSelection()
  129. updateDateLabel()
  130. }
  131. }
  132. var selectedStartTime: TimeInterval {
  133. let row = picker.selectedRow(inComponent: Component.time.rawValue)
  134. return startTimeForTimeComponent(row: row)
  135. }
  136. public var allowedTimeRange: ClosedRange<TimeInterval> = .hours(0)...(.hours(23.5)) {
  137. didSet {
  138. picker.reloadComponent(Component.time.rawValue)
  139. updateStartTimeSelection()
  140. }
  141. }
  142. var isPickerHidden: Bool {
  143. get {
  144. return picker.isHidden
  145. }
  146. set {
  147. picker.isHidden = newValue
  148. pickerHeightConstraint.constant = newValue ? 0 : pickerExpandedHeight
  149. if !newValue {
  150. selectPickerValues()
  151. }
  152. }
  153. }
  154. private func updateStartTimeSelection() {
  155. let row = Int(round((startTime - allowedTimeRange.lowerBound) / minimumTimeInterval))
  156. if row >= 0 && row < pickerView(picker, numberOfRowsInComponent: Component.time.rawValue) {
  157. picker.selectRow(row, inComponent: Component.time.rawValue, animated: true)
  158. }
  159. }
  160. fileprivate func selectPickerValue(for component: Component, with selectedValue: Double?, allowedValues: [Double]) {
  161. guard !allowedValues.isEmpty else {
  162. return
  163. }
  164. let selectedIndex: Int
  165. if let value = selectedValue {
  166. if let row = allowedValues.firstIndex(of: value) {
  167. selectedIndex = row
  168. } else {
  169. // Select next highest value
  170. selectedIndex = allowedValues.enumerated().filter({$0.element >= value}).min(by: { $0.1 < $1.1 })?.offset ?? 0
  171. }
  172. } else {
  173. selectedIndex = allowedValues.count
  174. }
  175. picker.selectRow(selectedIndex, inComponent: component.rawValue, animated: false)
  176. }
  177. fileprivate func selectPickerValues() {
  178. selectPickerValue(for: .minValue, with: minValue, allowedValues: allowedMinValues)
  179. selectPickerValue(for: .maxValue, with: maxValue, allowedValues: allowedMaxValues)
  180. }
  181. fileprivate func updateMinValueFromPicker() {
  182. let index = picker.selectedRow(inComponent: Component.minValue.rawValue)
  183. let value: Double?
  184. if index >= 0 && index < allowedMinValues.count {
  185. value = allowedMinValues[index]
  186. } else {
  187. value = nil
  188. }
  189. minValue = value
  190. }
  191. fileprivate func updateMaxValueFromPicker() {
  192. let index = picker.selectedRow(inComponent: Component.maxValue.rawValue)
  193. let value: Double?
  194. if index >= 0 && index < allowedMaxValues.count {
  195. value = allowedMaxValues[index]
  196. } else {
  197. value = nil
  198. }
  199. maxValue = value
  200. }
  201. func updateDateLabel() {
  202. dateLabel.text = stringForStartTime(startTime)
  203. }
  204. private func startTimeForTimeComponent(row: Int) -> TimeInterval {
  205. return allowedTimeRange.lowerBound + minimumTimeInterval * TimeInterval(row)
  206. }
  207. private func stringForStartTime(_ time: TimeInterval) -> String {
  208. let date = startOfDay.addingTimeInterval(time)
  209. return dateFormatter.string(from: date)
  210. }
  211. override func awakeFromNib() {
  212. super.awakeFromNib()
  213. pickerExpandedHeight = pickerHeightConstraint.constant
  214. setSelected(true, animated: false)
  215. updateDateLabel()
  216. }
  217. override func setSelected(_ selected: Bool, animated: Bool) {
  218. super.setSelected(selected, animated: animated)
  219. if selected {
  220. isPickerHidden.toggle()
  221. }
  222. }
  223. }
  224. extension GlucoseRangeTableViewCell: UIPickerViewDelegate {
  225. func pickerView(_ pickerView: UIPickerView,
  226. didSelectRow row: Int,
  227. inComponent componentRaw: Int) {
  228. let component = Component(rawValue: componentRaw)!
  229. switch component {
  230. case .time:
  231. startTime = selectedStartTime
  232. case .minValue:
  233. updateMinValueFromPicker()
  234. picker.reloadComponent(Component.minValue.rawValue)
  235. picker.reloadComponent(Component.maxValue.rawValue)
  236. selectPickerValues()
  237. case .maxValue:
  238. updateMaxValueFromPicker()
  239. picker.reloadComponent(Component.minValue.rawValue)
  240. picker.reloadComponent(Component.maxValue.rawValue)
  241. selectPickerValues()
  242. default:
  243. break
  244. }
  245. delegate?.glucoseRangeTableViewCellDidUpdate(self)
  246. }
  247. func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
  248. return [33,16,4,16,24][component] / 100.0 * picker.frame.width
  249. }
  250. func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
  251. let metrics = UIFontMetrics(forTextStyle: .body)
  252. return metrics.scaledValue(for: 32)
  253. }
  254. func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
  255. let title: String?
  256. let component = Component(rawValue: component)!
  257. var attributes: [NSAttributedString.Key : Any]? = nil
  258. switch component {
  259. case .time:
  260. let time = startTimeForTimeComponent(row: row)
  261. title = stringForStartTime(time)
  262. case .minValue:
  263. if row >= allowedMinValues.count {
  264. title = component.placeholderString
  265. attributes = [.foregroundColor: UIColor.lightGray]
  266. } else {
  267. title = valueNumberFormatter.string(from: allowedMinValues[row])
  268. }
  269. case .maxValue:
  270. if row >= allowedMaxValues.count {
  271. title = component.placeholderString
  272. attributes = [.foregroundColor: UIColor.lightGray]
  273. } else {
  274. title = valueNumberFormatter.string(from: allowedMaxValues[row])
  275. }
  276. case .separator:
  277. title = LocalizedString("-", comment: "Separator between min and max glucose values")
  278. case .units:
  279. title = unitString
  280. }
  281. if let title = title {
  282. return NSAttributedString(string: title, attributes: attributes)
  283. } else {
  284. return nil
  285. }
  286. }
  287. }
  288. extension GlucoseRangeTableViewCell: UIPickerViewDataSource {
  289. func numberOfComponents(in pickerView: UIPickerView) -> Int {
  290. return Component.allCases.count
  291. }
  292. func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
  293. switch Component(rawValue: component)! {
  294. case .time:
  295. if allowTimeSelection {
  296. return Int(round((allowedTimeRange.upperBound - allowedTimeRange.lowerBound) / minimumTimeInterval) + 1)
  297. } else {
  298. return 0
  299. }
  300. case .minValue:
  301. return allowedMinValues.count + (minValue != nil ? 0 : 1)
  302. case .maxValue:
  303. return allowedMaxValues.count + (maxValue != nil ? 0 : 1)
  304. case .units, .separator:
  305. return 1
  306. }
  307. }
  308. }
  309. /// UITableViewController extensions to aid working with DatePickerTableViewCell
  310. extension GlucoseRangeTableViewCellDelegate where Self: UITableViewController {
  311. func hideGlucoseRangeCells(excluding indexPath: IndexPath? = nil) {
  312. for case let cell as GlucoseRangeTableViewCell in tableView.visibleCells where tableView.indexPath(for: cell) != indexPath && cell.isPickerHidden == false {
  313. cell.isPickerHidden = true
  314. }
  315. }
  316. }