CarbEntryEditViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. //
  2. // CarbEntryEditViewController.swift
  3. // CarbKit
  4. //
  5. // Created by Nathan Racklyeft on 1/15/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. public final class CarbEntryEditViewController: UITableViewController {
  12. var navigationDelegate = CarbEntryNavigationDelegate()
  13. public var defaultAbsorptionTimes: CarbStore.DefaultAbsorptionTimes? {
  14. didSet {
  15. if let times = defaultAbsorptionTimes {
  16. orderedAbsorptionTimes = [times.fast, times.medium, times.slow]
  17. }
  18. }
  19. }
  20. fileprivate var orderedAbsorptionTimes = [TimeInterval]()
  21. public var preferredUnit = HKUnit.gram()
  22. public var maxQuantity = HKQuantity(unit: .gram(), doubleValue: 250)
  23. /// Entry configuration values. Must be set before presenting.
  24. public var absorptionTimePickerInterval = TimeInterval(minutes: 30)
  25. public var maxAbsorptionTime = TimeInterval(hours: 8)
  26. public var maximumDateFutureInterval = TimeInterval(hours: 4)
  27. public var originalCarbEntry: StoredCarbEntry? {
  28. didSet {
  29. if let entry = originalCarbEntry {
  30. quantity = entry.quantity
  31. date = entry.startDate
  32. foodType = entry.foodType
  33. absorptionTime = entry.absorptionTime
  34. absorptionTimeWasEdited = true
  35. usesCustomFoodType = true
  36. shouldBeginEditingQuantity = false
  37. }
  38. }
  39. }
  40. fileprivate var quantity: HKQuantity?
  41. fileprivate var date = Date()
  42. fileprivate var foodType: String?
  43. fileprivate var absorptionTime: TimeInterval?
  44. fileprivate var absorptionTimeWasEdited = false
  45. fileprivate var usesCustomFoodType = false
  46. private var shouldBeginEditingQuantity = true
  47. private var shouldBeginEditingFoodType = false
  48. public var updatedCarbEntry: NewCarbEntry? {
  49. if let quantity = quantity,
  50. let absorptionTime = absorptionTime ?? defaultAbsorptionTimes?.medium
  51. {
  52. if let o = originalCarbEntry, o.quantity == quantity && o.startDate == date && o.foodType == foodType && o.absorptionTime == absorptionTime {
  53. return nil // No changes were made
  54. }
  55. return NewCarbEntry(
  56. quantity: quantity,
  57. startDate: date,
  58. foodType: foodType,
  59. absorptionTime: absorptionTime,
  60. externalID: originalCarbEntry?.externalID
  61. )
  62. } else {
  63. return nil
  64. }
  65. }
  66. private var isSampleEditable: Bool {
  67. return originalCarbEntry?.createdByCurrentApp != false
  68. }
  69. public override func viewDidLoad() {
  70. super.viewDidLoad()
  71. tableView.rowHeight = UITableView.automaticDimension
  72. tableView.estimatedRowHeight = 44
  73. tableView.register(DateAndDurationTableViewCell.nib(), forCellReuseIdentifier: DateAndDurationTableViewCell.className)
  74. if originalCarbEntry != nil {
  75. title = LocalizedString("Edit Carb Entry", value: "Edit Carb Entry", comment: "The title of the view controller to edit an existing carb entry")
  76. } else {
  77. title = LocalizedString("Add Carb Entry", value: "Add Carb Entry", comment: "The title of the view controller to create a new carb entry")
  78. }
  79. }
  80. public override func viewDidAppear(_ animated: Bool) {
  81. super.viewDidAppear(animated)
  82. if shouldBeginEditingQuantity, let cell = tableView.cellForRow(at: IndexPath(row: Row.value.rawValue, section: 0)) as? DecimalTextFieldTableViewCell {
  83. shouldBeginEditingQuantity = false
  84. cell.textField.becomeFirstResponder()
  85. }
  86. }
  87. private var foodKeyboard: EmojiInputController!
  88. @IBOutlet weak var saveButtonItem: UIBarButtonItem!
  89. // MARK: - Table view data source
  90. fileprivate enum Row: Int {
  91. case value
  92. case date
  93. case foodType
  94. case absorptionTime
  95. static let count = 4
  96. }
  97. public override func numberOfSections(in tableView: UITableView) -> Int {
  98. return 1
  99. }
  100. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  101. return Row.count
  102. }
  103. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  104. switch Row(rawValue: indexPath.row)! {
  105. case .value:
  106. let cell = tableView.dequeueReusableCell(withIdentifier: DecimalTextFieldTableViewCell.className) as! DecimalTextFieldTableViewCell
  107. if let quantity = quantity {
  108. cell.number = NSNumber(value: quantity.doubleValue(for: preferredUnit))
  109. }
  110. cell.textField.isEnabled = isSampleEditable
  111. cell.unitLabel?.text = String(describing: preferredUnit)
  112. cell.delegate = self
  113. return cell
  114. case .date:
  115. let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className) as! DateAndDurationTableViewCell
  116. cell.titleLabel.text = LocalizedString("Date", comment: "Title of the carb entry date picker cell")
  117. cell.datePicker.isEnabled = isSampleEditable
  118. cell.datePicker.datePickerMode = .dateAndTime
  119. cell.datePicker.maximumDate = Date(timeIntervalSinceNow: maximumDateFutureInterval)
  120. cell.datePicker.minuteInterval = 1
  121. cell.date = date
  122. cell.delegate = self
  123. return cell
  124. case .foodType:
  125. if usesCustomFoodType {
  126. let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.className, for: indexPath) as! TextFieldTableViewCell
  127. cell.textField.text = foodType
  128. cell.delegate = self
  129. if let textField = cell.textField as? CustomInputTextField {
  130. if foodKeyboard == nil {
  131. foodKeyboard = CarbAbsorptionInputController()
  132. foodKeyboard.delegate = self
  133. }
  134. textField.customInput = foodKeyboard
  135. }
  136. return cell
  137. } else {
  138. let cell = tableView.dequeueReusableCell(withIdentifier: FoodTypeShortcutCell.className, for: indexPath) as! FoodTypeShortcutCell
  139. if absorptionTime == nil {
  140. cell.selectionState = .medium
  141. }
  142. cell.delegate = self
  143. return cell
  144. }
  145. case .absorptionTime:
  146. let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className) as! DateAndDurationTableViewCell
  147. cell.titleLabel.text = LocalizedString("Absorption Time", comment: "Title of the carb entry absorption time cell")
  148. cell.datePicker.isEnabled = isSampleEditable
  149. cell.datePicker.datePickerMode = .countDownTimer
  150. cell.datePicker.minuteInterval = Int(absorptionTimePickerInterval.minutes)
  151. if let duration = absorptionTime ?? defaultAbsorptionTimes?.medium {
  152. cell.duration = duration
  153. }
  154. cell.maximumDuration = maxAbsorptionTime
  155. cell.delegate = self
  156. return cell
  157. }
  158. }
  159. public override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  160. switch Row(rawValue: indexPath.row)! {
  161. case .value, .date:
  162. break
  163. case .foodType:
  164. if usesCustomFoodType, shouldBeginEditingFoodType, let cell = cell as? TextFieldTableViewCell {
  165. shouldBeginEditingFoodType = false
  166. cell.textField.becomeFirstResponder()
  167. }
  168. case .absorptionTime:
  169. break
  170. }
  171. }
  172. public override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  173. return LocalizedString("Choose a longer absorption time for larger meals, or those containing fats and proteins. This is only guidance to the algorithm and need not be exact.", comment: "Carb entry section footer text explaining absorption time")
  174. }
  175. // MARK: - UITableViewDelegate
  176. public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  177. tableView.endEditing(false)
  178. tableView.beginUpdates()
  179. hideDatePickerCells(excluding: indexPath)
  180. return indexPath
  181. }
  182. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  183. switch tableView.cellForRow(at: indexPath) {
  184. case is FoodTypeShortcutCell:
  185. usesCustomFoodType = true
  186. shouldBeginEditingFoodType = true
  187. tableView.reloadRows(at: [IndexPath(row: Row.foodType.rawValue, section: 0)], with: .none)
  188. default:
  189. break
  190. }
  191. tableView.endUpdates()
  192. tableView.deselectRow(at: indexPath, animated: true)
  193. }
  194. // MARK: - Navigation
  195. public override func restoreUserActivityState(_ activity: NSUserActivity) {
  196. if let entry = activity.newCarbEntry {
  197. quantity = entry.quantity
  198. date = entry.startDate
  199. if let foodType = entry.foodType {
  200. self.foodType = foodType
  201. usesCustomFoodType = true
  202. }
  203. if let absorptionTime = entry.absorptionTime {
  204. self.absorptionTime = absorptionTime
  205. absorptionTimeWasEdited = true
  206. }
  207. }
  208. }
  209. public override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
  210. self.tableView.endEditing(true)
  211. guard let button = sender as? UIBarButtonItem, button == saveButtonItem else {
  212. quantity = nil
  213. return super.shouldPerformSegue(withIdentifier: identifier, sender: sender)
  214. }
  215. guard let absorptionTime = absorptionTime ?? defaultAbsorptionTimes?.medium else {
  216. return false
  217. }
  218. guard absorptionTime <= maxAbsorptionTime else {
  219. navigationDelegate.showAbsorptionTimeValidationWarning(for: self, maxAbsorptionTime: maxAbsorptionTime)
  220. return false
  221. }
  222. guard let quantity = quantity, quantity.doubleValue(for: HKUnit.gram()) > 0 else { return false }
  223. guard quantity.compare(maxQuantity) != .orderedDescending else {
  224. navigationDelegate.showMaxQuantityValidationWarning(for: self, maxQuantityGrams: maxQuantity.doubleValue(for: .gram()))
  225. return false
  226. }
  227. return true
  228. }
  229. }
  230. extension CarbEntryEditViewController: TextFieldTableViewCellDelegate {
  231. public func textFieldTableViewCellDidBeginEditing(_ cell: TextFieldTableViewCell) {
  232. // Collapse any date picker cells to save space
  233. tableView.beginUpdates()
  234. hideDatePickerCells()
  235. tableView.endUpdates()
  236. }
  237. public func textFieldTableViewCellDidEndEditing(_ cell: TextFieldTableViewCell) {
  238. guard let row = tableView.indexPath(for: cell)?.row else { return }
  239. switch Row(rawValue: row) {
  240. case .value?:
  241. if let cell = cell as? DecimalTextFieldTableViewCell, let number = cell.number {
  242. quantity = HKQuantity(unit: preferredUnit, doubleValue: number.doubleValue)
  243. } else {
  244. quantity = nil
  245. }
  246. case .foodType?:
  247. foodType = cell.textField.text
  248. default:
  249. break
  250. }
  251. }
  252. }
  253. extension CarbEntryEditViewController: DatePickerTableViewCellDelegate {
  254. public func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
  255. guard let row = tableView.indexPath(for: cell)?.row else { return }
  256. switch Row(rawValue: row) {
  257. case .date?:
  258. date = cell.date
  259. case .absorptionTime?:
  260. absorptionTime = cell.duration
  261. absorptionTimeWasEdited = true
  262. default:
  263. break
  264. }
  265. }
  266. }
  267. extension CarbEntryEditViewController: FoodTypeShortcutCellDelegate {
  268. func foodTypeShortcutCellDidUpdateSelection(_ cell: FoodTypeShortcutCell) {
  269. var absorptionTime: TimeInterval?
  270. switch cell.selectionState {
  271. case .fast:
  272. absorptionTime = defaultAbsorptionTimes?.fast
  273. case .medium:
  274. absorptionTime = defaultAbsorptionTimes?.medium
  275. case .slow:
  276. absorptionTime = defaultAbsorptionTimes?.slow
  277. case .custom:
  278. tableView.beginUpdates()
  279. usesCustomFoodType = true
  280. shouldBeginEditingFoodType = true
  281. tableView.reloadRows(at: [IndexPath(row: Row.foodType.rawValue, section: 0)], with: .fade)
  282. tableView.endUpdates()
  283. }
  284. if let absorptionTime = absorptionTime {
  285. self.absorptionTime = absorptionTime
  286. if let cell = tableView.cellForRow(at: IndexPath(row: Row.absorptionTime.rawValue, section: 0)) as? DateAndDurationTableViewCell {
  287. cell.duration = absorptionTime
  288. }
  289. }
  290. }
  291. }
  292. extension CarbEntryEditViewController: EmojiInputControllerDelegate {
  293. func emojiInputControllerDidAdvanceToStandardInputMode(_ controller: EmojiInputController) {
  294. if let cell = tableView.cellForRow(at: IndexPath(row: Row.foodType.rawValue, section: 0)) as? TextFieldTableViewCell, let textField = cell.textField as? CustomInputTextField, textField.customInput != nil {
  295. let customInput = textField.customInput
  296. textField.customInput = nil
  297. textField.resignFirstResponder()
  298. textField.becomeFirstResponder()
  299. textField.customInput = customInput
  300. }
  301. }
  302. func emojiInputControllerDidSelectItemInSection(_ section: Int) {
  303. guard !absorptionTimeWasEdited, section < orderedAbsorptionTimes.count else {
  304. return
  305. }
  306. let lastAbsorptionTime = self.absorptionTime
  307. self.absorptionTime = orderedAbsorptionTimes[section]
  308. if let cell = tableView.cellForRow(at: IndexPath(row: Row.absorptionTime.rawValue, section: 0)) as? DateAndDurationTableViewCell {
  309. cell.duration = max(lastAbsorptionTime ?? 0, orderedAbsorptionTimes[section])
  310. }
  311. }
  312. }