CarbEntryEditViewController.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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 lastEntryDate: Date?
  41. fileprivate func updateLastEntryDate() { lastEntryDate = Date() }
  42. fileprivate var quantity: HKQuantity? {
  43. didSet {
  44. if quantity != oldValue {
  45. updateLastEntryDate()
  46. }
  47. }
  48. }
  49. fileprivate var date = Date() {
  50. didSet {
  51. if date != oldValue {
  52. updateLastEntryDate()
  53. }
  54. }
  55. }
  56. fileprivate var foodType: String? {
  57. didSet {
  58. if foodType != oldValue {
  59. updateLastEntryDate()
  60. }
  61. }
  62. }
  63. fileprivate var absorptionTime: TimeInterval? {
  64. didSet {
  65. if absorptionTime != oldValue {
  66. updateLastEntryDate()
  67. }
  68. }
  69. }
  70. fileprivate var absorptionTimeWasEdited = false
  71. fileprivate var usesCustomFoodType = false
  72. private var shouldBeginEditingQuantity = true
  73. private var shouldBeginEditingFoodType = false
  74. public var updatedCarbEntry: NewCarbEntry? {
  75. if let lastEntryDate = lastEntryDate,
  76. let quantity = quantity,
  77. let absorptionTime = absorptionTime ?? defaultAbsorptionTimes?.medium
  78. {
  79. if let o = originalCarbEntry, o.quantity == quantity && o.startDate == date && o.foodType == foodType && o.absorptionTime == absorptionTime {
  80. return nil // No changes were made
  81. }
  82. return NewCarbEntry(
  83. date: lastEntryDate,
  84. quantity: quantity,
  85. startDate: date,
  86. foodType: foodType,
  87. absorptionTime: absorptionTime
  88. )
  89. } else {
  90. return nil
  91. }
  92. }
  93. private var isSampleEditable: Bool {
  94. return originalCarbEntry?.createdByCurrentApp != false
  95. }
  96. public override func viewDidLoad() {
  97. super.viewDidLoad()
  98. tableView.rowHeight = UITableView.automaticDimension
  99. tableView.estimatedRowHeight = 44
  100. tableView.register(DateAndDurationTableViewCell.nib(), forCellReuseIdentifier: DateAndDurationTableViewCell.className)
  101. if originalCarbEntry != nil {
  102. title = LocalizedString("Edit Carb Entry", value: "Edit Carb Entry", comment: "The title of the view controller to edit an existing carb entry")
  103. } else {
  104. title = LocalizedString("Add Carb Entry", value: "Add Carb Entry", comment: "The title of the view controller to create a new carb entry")
  105. }
  106. }
  107. public override func viewDidAppear(_ animated: Bool) {
  108. super.viewDidAppear(animated)
  109. if shouldBeginEditingQuantity, let cell = tableView.cellForRow(at: IndexPath(row: Row.value.rawValue, section: 0)) as? DecimalTextFieldTableViewCell {
  110. shouldBeginEditingQuantity = false
  111. cell.textField.becomeFirstResponder()
  112. }
  113. }
  114. private var foodKeyboard: EmojiInputController!
  115. @IBOutlet weak var saveButtonItem: UIBarButtonItem!
  116. // MARK: - Table view data source
  117. fileprivate enum Row: Int {
  118. case value
  119. case date
  120. case foodType
  121. case absorptionTime
  122. static let count = 4
  123. }
  124. public override func numberOfSections(in tableView: UITableView) -> Int {
  125. return 1
  126. }
  127. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  128. return Row.count
  129. }
  130. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  131. switch Row(rawValue: indexPath.row)! {
  132. case .value:
  133. let cell = tableView.dequeueReusableCell(withIdentifier: DecimalTextFieldTableViewCell.className) as! DecimalTextFieldTableViewCell
  134. if let quantity = quantity {
  135. cell.number = NSNumber(value: quantity.doubleValue(for: preferredUnit))
  136. }
  137. cell.textField.isEnabled = isSampleEditable
  138. cell.unitLabel?.text = String(describing: preferredUnit)
  139. cell.delegate = self
  140. return cell
  141. case .date:
  142. let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className) as! DateAndDurationTableViewCell
  143. cell.titleLabel.text = LocalizedString("Date", comment: "Title of the carb entry date picker cell")
  144. cell.datePicker.isEnabled = isSampleEditable
  145. cell.datePicker.datePickerMode = .dateAndTime
  146. cell.datePicker.preferredDatePickerStyle = .wheels
  147. cell.datePicker.maximumDate = Date(timeIntervalSinceNow: maximumDateFutureInterval)
  148. cell.datePicker.minuteInterval = 1
  149. cell.date = date
  150. cell.delegate = self
  151. return cell
  152. case .foodType:
  153. if usesCustomFoodType {
  154. let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.className, for: indexPath) as! TextFieldTableViewCell
  155. cell.textField.text = foodType
  156. cell.delegate = self
  157. if let textField = cell.textField as? CustomInputTextField {
  158. if foodKeyboard == nil {
  159. foodKeyboard = CarbAbsorptionInputController()
  160. foodKeyboard.delegate = self
  161. }
  162. textField.customInput = foodKeyboard
  163. }
  164. return cell
  165. } else {
  166. let cell = tableView.dequeueReusableCell(withIdentifier: FoodTypeShortcutCell.className, for: indexPath) as! FoodTypeShortcutCell
  167. if absorptionTime == nil {
  168. cell.selectionState = .medium
  169. }
  170. cell.delegate = self
  171. return cell
  172. }
  173. case .absorptionTime:
  174. let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className) as! DateAndDurationTableViewCell
  175. cell.titleLabel.text = LocalizedString("Absorption Time", comment: "Title of the carb entry absorption time cell")
  176. cell.datePicker.isEnabled = isSampleEditable
  177. cell.datePicker.datePickerMode = .countDownTimer
  178. cell.datePicker.minuteInterval = Int(absorptionTimePickerInterval.minutes)
  179. if let duration = absorptionTime ?? defaultAbsorptionTimes?.medium {
  180. cell.duration = duration
  181. }
  182. cell.maximumDuration = maxAbsorptionTime
  183. cell.delegate = self
  184. return cell
  185. }
  186. }
  187. public override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  188. switch Row(rawValue: indexPath.row)! {
  189. case .value, .date:
  190. break
  191. case .foodType:
  192. if usesCustomFoodType, shouldBeginEditingFoodType, let cell = cell as? TextFieldTableViewCell {
  193. shouldBeginEditingFoodType = false
  194. cell.textField.becomeFirstResponder()
  195. }
  196. case .absorptionTime:
  197. break
  198. }
  199. }
  200. public override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  201. 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")
  202. }
  203. // MARK: - UITableViewDelegate
  204. public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  205. tableView.endEditing(false)
  206. tableView.beginUpdates()
  207. hideDatePickerCells(excluding: indexPath)
  208. return indexPath
  209. }
  210. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  211. switch tableView.cellForRow(at: indexPath) {
  212. case is FoodTypeShortcutCell:
  213. usesCustomFoodType = true
  214. shouldBeginEditingFoodType = true
  215. tableView.reloadRows(at: [IndexPath(row: Row.foodType.rawValue, section: 0)], with: .none)
  216. default:
  217. break
  218. }
  219. tableView.endUpdates()
  220. tableView.deselectRow(at: indexPath, animated: true)
  221. }
  222. // MARK: - Navigation
  223. public override func restoreUserActivityState(_ activity: NSUserActivity) {
  224. if let entry = activity.newCarbEntry {
  225. lastEntryDate = entry.date
  226. quantity = entry.quantity
  227. date = entry.startDate
  228. if let foodType = entry.foodType {
  229. self.foodType = foodType
  230. usesCustomFoodType = true
  231. }
  232. if let absorptionTime = entry.absorptionTime {
  233. self.absorptionTime = absorptionTime
  234. absorptionTimeWasEdited = true
  235. }
  236. }
  237. }
  238. public override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
  239. self.tableView.endEditing(true)
  240. guard let button = sender as? UIBarButtonItem, button == saveButtonItem else {
  241. quantity = nil
  242. return super.shouldPerformSegue(withIdentifier: identifier, sender: sender)
  243. }
  244. guard let absorptionTime = absorptionTime ?? defaultAbsorptionTimes?.medium else {
  245. return false
  246. }
  247. guard absorptionTime <= maxAbsorptionTime else {
  248. navigationDelegate.showAbsorptionTimeValidationWarning(for: self, maxAbsorptionTime: maxAbsorptionTime)
  249. return false
  250. }
  251. guard let quantity = quantity, quantity.doubleValue(for: HKUnit.gram()) > 0 else { return false }
  252. guard quantity.compare(maxQuantity) != .orderedDescending else {
  253. navigationDelegate.showMaxQuantityValidationWarning(for: self, maxQuantityGrams: maxQuantity.doubleValue(for: .gram()))
  254. return false
  255. }
  256. return true
  257. }
  258. }
  259. extension CarbEntryEditViewController: TextFieldTableViewCellDelegate {
  260. public func textFieldTableViewCellDidBeginEditing(_ cell: TextFieldTableViewCell) {
  261. // Collapse any date picker cells to save space
  262. tableView.beginUpdates()
  263. hideDatePickerCells()
  264. tableView.endUpdates()
  265. }
  266. public func textFieldTableViewCellDidChangeEditing(_ cell: TextFieldTableViewCell) {
  267. guard let row = tableView.indexPath(for: cell)?.row else { return }
  268. switch Row(rawValue: row) {
  269. case .value?:
  270. if let cell = cell as? DecimalTextFieldTableViewCell, let number = cell.number {
  271. quantity = HKQuantity(unit: preferredUnit, doubleValue: number.doubleValue)
  272. } else {
  273. quantity = nil
  274. }
  275. case .foodType?:
  276. foodType = cell.textField.text
  277. default:
  278. break
  279. }
  280. }
  281. public func textFieldTableViewCellDidEndEditing(_ cell: TextFieldTableViewCell) {
  282. textFieldTableViewCellDidChangeEditing(cell)
  283. }
  284. }
  285. extension CarbEntryEditViewController: DatePickerTableViewCellDelegate {
  286. public func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) {
  287. guard let row = tableView.indexPath(for: cell)?.row else { return }
  288. switch Row(rawValue: row) {
  289. case .date?:
  290. date = cell.date
  291. case .absorptionTime?:
  292. absorptionTime = cell.duration
  293. absorptionTimeWasEdited = true
  294. default:
  295. break
  296. }
  297. }
  298. }
  299. extension CarbEntryEditViewController: FoodTypeShortcutCellDelegate {
  300. public func foodTypeShortcutCellDidUpdateSelection(_ cell: FoodTypeShortcutCell) {
  301. var absorptionTime: TimeInterval?
  302. switch cell.selectionState {
  303. case .fast:
  304. absorptionTime = defaultAbsorptionTimes?.fast
  305. case .medium:
  306. absorptionTime = defaultAbsorptionTimes?.medium
  307. case .slow:
  308. absorptionTime = defaultAbsorptionTimes?.slow
  309. case .custom:
  310. tableView.beginUpdates()
  311. usesCustomFoodType = true
  312. shouldBeginEditingFoodType = true
  313. tableView.reloadRows(at: [IndexPath(row: Row.foodType.rawValue, section: 0)], with: .fade)
  314. tableView.endUpdates()
  315. }
  316. if let absorptionTime = absorptionTime {
  317. self.absorptionTime = absorptionTime
  318. if let cell = tableView.cellForRow(at: IndexPath(row: Row.absorptionTime.rawValue, section: 0)) as? DateAndDurationTableViewCell {
  319. cell.duration = absorptionTime
  320. }
  321. }
  322. }
  323. }
  324. extension CarbEntryEditViewController: EmojiInputControllerDelegate {
  325. public func emojiInputControllerDidAdvanceToStandardInputMode(_ controller: EmojiInputController) {
  326. if let cell = tableView.cellForRow(at: IndexPath(row: Row.foodType.rawValue, section: 0)) as? TextFieldTableViewCell, let textField = cell.textField as? CustomInputTextField, textField.customInput != nil {
  327. let customInput = textField.customInput
  328. textField.customInput = nil
  329. textField.resignFirstResponder()
  330. textField.becomeFirstResponder()
  331. textField.customInput = customInput
  332. }
  333. }
  334. public func emojiInputControllerDidSelectItemInSection(_ section: Int) {
  335. guard !absorptionTimeWasEdited, section < orderedAbsorptionTimes.count else {
  336. return
  337. }
  338. if absorptionTime == nil {
  339. // only adjust the absorption time if it wasn't already set.
  340. absorptionTime = orderedAbsorptionTimes[section]
  341. if let cell = tableView.cellForRow(at: IndexPath(row: Row.absorptionTime.rawValue, section: 0)) as? DateAndDurationTableViewCell {
  342. cell.duration = orderedAbsorptionTimes[section]
  343. }
  344. }
  345. }
  346. }