GlucoseRangeTableViewCell.swift 12 KB

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