CarbEntryTableViewController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. //
  2. // CarbEntryTableViewController.swift
  3. // CarbKit
  4. //
  5. // Created by Nathan Racklyeft on 1/10/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import HealthKit
  10. import LoopKit
  11. private let ReuseIdentifier = "CarbEntry"
  12. public final class CarbEntryTableViewController: UITableViewController {
  13. @IBOutlet var unavailableMessageView: UIView!
  14. @IBOutlet var authorizationRequiredMessageView: UIView!
  15. @IBOutlet weak var COBValueLabel: UILabel!
  16. @IBOutlet weak var COBDateLabel: UILabel!
  17. @IBOutlet weak var totalValueLabel: UILabel!
  18. @IBOutlet weak var totalDateLabel: UILabel!
  19. public var carbStore: CarbStore? {
  20. didSet {
  21. if let carbStore = carbStore {
  22. carbStoreObserver = NotificationCenter.default.addObserver(forName: nil,
  23. object: carbStore,
  24. queue: OperationQueue.main,
  25. using: { [weak self] (note) -> Void in
  26. switch note.name {
  27. case CarbStore.carbEntriesDidChange:
  28. if let strongSelf = self, strongSelf.isViewLoaded {
  29. strongSelf.reloadData()
  30. }
  31. case Notification.Name.StoreAuthorizationStatusDidChange:
  32. break
  33. default:
  34. break
  35. }
  36. }
  37. )
  38. } else {
  39. carbStoreObserver = nil
  40. }
  41. }
  42. }
  43. private var updateTimer: Timer? {
  44. willSet {
  45. if let timer = updateTimer {
  46. timer.invalidate()
  47. }
  48. }
  49. }
  50. public override func viewDidLoad() {
  51. super.viewDidLoad()
  52. if let carbStore = carbStore {
  53. if carbStore.authorizationRequired {
  54. state = .authorizationRequired
  55. } else if carbStore.sharingDenied {
  56. state = .unavailable
  57. } else {
  58. state = .display
  59. }
  60. } else {
  61. state = .unavailable
  62. }
  63. navigationItem.rightBarButtonItems?.append(editButtonItem)
  64. }
  65. public override func viewWillAppear(_ animated: Bool) {
  66. super.viewWillAppear(animated)
  67. updateTimelyStats(nil)
  68. }
  69. public override func viewDidAppear(_ animated: Bool) {
  70. super.viewDidAppear(animated)
  71. let updateInterval = TimeInterval(minutes: 5)
  72. let timer = Timer(
  73. fireAt: Date().dateCeiledToTimeInterval(updateInterval).addingTimeInterval(2),
  74. interval: updateInterval,
  75. target: self,
  76. selector: #selector(updateTimelyStats(_:)),
  77. userInfo: nil,
  78. repeats: true
  79. )
  80. updateTimer = timer
  81. RunLoop.current.add(timer, forMode: RunLoop.Mode.default)
  82. }
  83. public override func viewWillDisappear(_ animated: Bool) {
  84. super.viewWillDisappear(animated)
  85. updateTimer = nil
  86. }
  87. deinit {
  88. if let observer = carbStoreObserver {
  89. NotificationCenter.default.removeObserver(observer)
  90. }
  91. }
  92. // MARK: - Data
  93. private var carbEntries: [StoredCarbEntry] = []
  94. private enum State {
  95. case unknown
  96. case unavailable
  97. case authorizationRequired
  98. case display
  99. }
  100. private var state = State.unknown {
  101. didSet {
  102. if isViewLoaded {
  103. reloadData()
  104. }
  105. }
  106. }
  107. private func reloadData() {
  108. switch state {
  109. case .unknown:
  110. break
  111. case .unavailable:
  112. tableView.backgroundView = unavailableMessageView
  113. case .authorizationRequired:
  114. tableView.backgroundView = authorizationRequiredMessageView
  115. case .display:
  116. navigationItem.rightBarButtonItems?.forEach { $0.isEnabled = true }
  117. tableView.backgroundView = nil
  118. tableView.tableHeaderView?.isHidden = false
  119. tableView.tableFooterView = nil
  120. guard let carbStore = carbStore else { return }
  121. let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -2 * carbStore.defaultAbsorptionTimes.slow))
  122. carbStore.getCarbEntries(start: start) { (result) in
  123. DispatchQueue.main.async {
  124. switch result {
  125. case .success(let entries):
  126. self.carbEntries = entries
  127. self.tableView.reloadData()
  128. case .failure(let error):
  129. self.present(UIAlertController(with: error), animated: true)
  130. }
  131. self.updateTimelyStats(nil)
  132. self.updateTotal()
  133. }
  134. }
  135. }
  136. }
  137. @objc func updateTimelyStats(_: Timer?) {
  138. updateCOB()
  139. }
  140. private func updateCOB() {
  141. if case .display = state, let carbStore = carbStore {
  142. carbStore.carbsOnBoard(at: Date()) { (result) in
  143. DispatchQueue.main.async {
  144. switch result {
  145. case .success(let value):
  146. self.COBValueLabel.text = NumberFormatter.localizedString(from: NSNumber(value: value.quantity.doubleValue(for: carbStore.preferredUnit)), number: .none)
  147. self.COBDateLabel.text = String(format: LocalizedString("com.loudnate.CarbKit.COBDateLabel", value: "at %1$@", comment: "The format string describing the date of a COB value. The first format argument is the localized date."), DateFormatter.localizedString(from: value.startDate, dateStyle: .none, timeStyle: .short))
  148. case .failure:
  149. self.COBValueLabel.text = NumberFormatter.localizedString(from: 0, number: .none)
  150. self.COBDateLabel.text = nil
  151. }
  152. }
  153. }
  154. }
  155. }
  156. private func updateTotal() {
  157. if case .display = state, let carbStore = carbStore {
  158. carbStore.getTotalCarbs(since: Calendar.current.startOfDay(for: Date())) { (result) -> Void in
  159. DispatchQueue.main.async {
  160. switch result {
  161. case .success(let value):
  162. self.totalValueLabel.text = NumberFormatter.localizedString(from: NSNumber(value: value.quantity.doubleValue(for: carbStore.preferredUnit)), number: .none)
  163. self.totalDateLabel.text = String(format: LocalizedString("com.loudnate.CarbKit.totalDateLabel", value: "since %1$@", comment: "The format string describing the starting date of a total value. The first format argument is the localized date."), DateFormatter.localizedString(from: value.startDate, dateStyle: .none, timeStyle: .short))
  164. case .failure:
  165. self.totalValueLabel.text = NumberFormatter.localizedString(from: 0, number: .none)
  166. self.totalDateLabel.text = nil
  167. }
  168. }
  169. }
  170. }
  171. }
  172. private var carbStoreObserver: Any? {
  173. willSet {
  174. if let observer = carbStoreObserver {
  175. NotificationCenter.default.removeObserver(observer)
  176. }
  177. }
  178. }
  179. // MARK: - Table view data source
  180. public override func numberOfSections(in tableView: UITableView) -> Int {
  181. switch state {
  182. case .unknown, .unavailable, .authorizationRequired:
  183. return 0
  184. case .display:
  185. return 1
  186. }
  187. }
  188. public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  189. return carbEntries.count
  190. }
  191. public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  192. let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier, for: indexPath)
  193. if case .display = state, let carbStore = carbStore {
  194. let entry = carbEntries[indexPath.row]
  195. let value = NumberFormatter.localizedString(from: NSNumber(value: entry.quantity.doubleValue(for: carbStore.preferredUnit)), number: .none)
  196. var titleText = "\(value) \(carbStore.preferredUnit!)"
  197. if let foodType = entry.foodType {
  198. titleText += ": \(foodType)"
  199. }
  200. cell.textLabel?.text = titleText
  201. var detailText = DateFormatter.localizedString(from: entry.startDate, dateStyle: .none, timeStyle: .short)
  202. if let absorptionTime = entry.absorptionTime {
  203. let minutes = NumberFormatter.localizedString(from: NSNumber(value: absorptionTime.minutes), number: .none)
  204. detailText += " + \(minutes) min"
  205. }
  206. cell.detailTextLabel?.text = detailText
  207. }
  208. return cell
  209. }
  210. public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
  211. return carbEntries[indexPath.row].createdByCurrentApp
  212. }
  213. public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
  214. if editingStyle == .delete, case .display = state, let carbStore = carbStore {
  215. let entry = carbEntries.remove(at: indexPath.row)
  216. carbStore.deleteCarbEntry(entry) { (result) -> Void in
  217. DispatchQueue.main.async {
  218. switch result {
  219. case .failure(let error):
  220. self.present(UIAlertController(with: error), animated: true)
  221. case .success:
  222. tableView.deleteRows(at: [indexPath], with: .automatic)
  223. self.updateTimelyStats(nil)
  224. self.updateTotal()
  225. }
  226. }
  227. }
  228. }
  229. }
  230. // MARK: - UITableViewDelegate
  231. public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  232. let entry = carbEntries[indexPath.row]
  233. if !entry.createdByCurrentApp {
  234. return nil
  235. }
  236. return indexPath
  237. }
  238. // MARK: - Navigation
  239. @IBAction func unwindFromEditing(_ segue: UIStoryboardSegue) {
  240. if let editVC = segue.source as? CarbEntryEditViewController,
  241. let updatedEntry = editVC.updatedCarbEntry
  242. {
  243. if let originalEntry = editVC.originalCarbEntry {
  244. carbStore?.replaceCarbEntry(originalEntry, withEntry: updatedEntry) { (result) -> Void in
  245. DispatchQueue.main.async {
  246. switch result {
  247. case .failure(let error):
  248. self.present(UIAlertController(with: error), animated: true)
  249. case .success:
  250. self.reloadData()
  251. }
  252. }
  253. }
  254. } else {
  255. carbStore?.addCarbEntry(updatedEntry) { (result) -> Void in
  256. DispatchQueue.main.async {
  257. switch result {
  258. case .failure(let error):
  259. self.present(UIAlertController(with: error), animated: true)
  260. case .success:
  261. self.reloadData()
  262. }
  263. }
  264. }
  265. }
  266. }
  267. }
  268. public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  269. var editVC = segue.destination as? CarbEntryEditViewController
  270. if editVC == nil, let navVC = segue.destination as? UINavigationController {
  271. editVC = navVC.viewControllers.first as? CarbEntryEditViewController
  272. }
  273. if let editVC = editVC {
  274. if let selectedCell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: selectedCell), indexPath.row < carbEntries.count {
  275. editVC.originalCarbEntry = carbEntries[indexPath.row]
  276. }
  277. editVC.defaultAbsorptionTimes = carbStore?.defaultAbsorptionTimes
  278. }
  279. }
  280. @IBAction func authorizeHealth(_ sender: Any) {
  281. if case .authorizationRequired = state, let carbStore = carbStore {
  282. carbStore.authorize { (result) in
  283. DispatchQueue.main.async {
  284. switch result {
  285. case .success:
  286. self.state = .display
  287. case .failure(let error):
  288. self.present(UIAlertController(with: error), animated: true)
  289. }
  290. }
  291. }
  292. }
  293. }
  294. }