// // CarbEntryTableViewController.swift // CarbKit // // Created by Nathan Racklyeft on 1/10/16. // Copyright © 2016 Nathan Racklyeft. All rights reserved. // import UIKit import HealthKit import LoopKit private let ReuseIdentifier = "CarbEntry" public final class CarbEntryTableViewController: UITableViewController { @IBOutlet var unavailableMessageView: UIView! @IBOutlet var authorizationRequiredMessageView: UIView! @IBOutlet weak var COBValueLabel: UILabel! @IBOutlet weak var COBDateLabel: UILabel! @IBOutlet weak var totalValueLabel: UILabel! @IBOutlet weak var totalDateLabel: UILabel! public var carbStore: CarbStore? { didSet { if let carbStore = carbStore { carbStoreObserver = NotificationCenter.default.addObserver(forName: nil, object: carbStore, queue: OperationQueue.main, using: { [weak self] (note) -> Void in switch note.name { case CarbStore.carbEntriesDidChange: if let strongSelf = self, strongSelf.isViewLoaded { strongSelf.reloadData() } case Notification.Name.StoreAuthorizationStatusDidChange: break default: break } } ) } else { carbStoreObserver = nil } } } private var updateTimer: Timer? { willSet { if let timer = updateTimer { timer.invalidate() } } } public override func viewDidLoad() { super.viewDidLoad() if let carbStore = carbStore { if carbStore.authorizationRequired { state = .authorizationRequired } else if carbStore.sharingDenied { state = .unavailable } else { state = .display } } else { state = .unavailable } navigationItem.rightBarButtonItems?.append(editButtonItem) } public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateTimelyStats(nil) } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let updateInterval = TimeInterval(minutes: 5) let timer = Timer( fireAt: Date().dateCeiledToTimeInterval(updateInterval).addingTimeInterval(2), interval: updateInterval, target: self, selector: #selector(updateTimelyStats(_:)), userInfo: nil, repeats: true ) updateTimer = timer RunLoop.current.add(timer, forMode: RunLoop.Mode.default) } public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) updateTimer = nil } deinit { if let observer = carbStoreObserver { NotificationCenter.default.removeObserver(observer) } } // MARK: - Data private var carbEntries: [StoredCarbEntry] = [] private enum State { case unknown case unavailable case authorizationRequired case display } private var state = State.unknown { didSet { if isViewLoaded { reloadData() } } } private func reloadData() { switch state { case .unknown: break case .unavailable: tableView.backgroundView = unavailableMessageView case .authorizationRequired: tableView.backgroundView = authorizationRequiredMessageView case .display: navigationItem.rightBarButtonItems?.forEach { $0.isEnabled = true } tableView.backgroundView = nil tableView.tableHeaderView?.isHidden = false tableView.tableFooterView = nil guard let carbStore = carbStore else { return } let start = min(Calendar.current.startOfDay(for: Date()), Date(timeIntervalSinceNow: -2 * carbStore.defaultAbsorptionTimes.slow)) carbStore.getCarbEntries(start: start) { (result) in DispatchQueue.main.async { switch result { case .success(let entries): self.carbEntries = entries self.tableView.reloadData() case .failure(let error): self.present(UIAlertController(with: error), animated: true) } self.updateTimelyStats(nil) self.updateTotal() } } } } @objc func updateTimelyStats(_: Timer?) { updateCOB() } private func updateCOB() { if case .display = state, let carbStore = carbStore { carbStore.carbsOnBoard(at: Date()) { (result) in DispatchQueue.main.async { switch result { case .success(let value): self.COBValueLabel.text = NumberFormatter.localizedString(from: NSNumber(value: value.quantity.doubleValue(for: carbStore.preferredUnit)), number: .none) 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)) case .failure: self.COBValueLabel.text = NumberFormatter.localizedString(from: 0, number: .none) self.COBDateLabel.text = nil } } } } } private func updateTotal() { if case .display = state, let carbStore = carbStore { carbStore.getTotalCarbs(since: Calendar.current.startOfDay(for: Date())) { (result) -> Void in DispatchQueue.main.async { switch result { case .success(let value): self.totalValueLabel.text = NumberFormatter.localizedString(from: NSNumber(value: value.quantity.doubleValue(for: carbStore.preferredUnit)), number: .none) 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)) case .failure: self.totalValueLabel.text = NumberFormatter.localizedString(from: 0, number: .none) self.totalDateLabel.text = nil } } } } } private var carbStoreObserver: Any? { willSet { if let observer = carbStoreObserver { NotificationCenter.default.removeObserver(observer) } } } // MARK: - Table view data source public override func numberOfSections(in tableView: UITableView) -> Int { switch state { case .unknown, .unavailable, .authorizationRequired: return 0 case .display: return 1 } } public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return carbEntries.count } public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier, for: indexPath) if case .display = state, let carbStore = carbStore { let entry = carbEntries[indexPath.row] let value = NumberFormatter.localizedString(from: NSNumber(value: entry.quantity.doubleValue(for: carbStore.preferredUnit)), number: .none) var titleText = "\(value) \(carbStore.preferredUnit!)" if let foodType = entry.foodType { titleText += ": \(foodType)" } cell.textLabel?.text = titleText var detailText = DateFormatter.localizedString(from: entry.startDate, dateStyle: .none, timeStyle: .short) if let absorptionTime = entry.absorptionTime { let minutes = NumberFormatter.localizedString(from: NSNumber(value: absorptionTime.minutes), number: .none) detailText += " + \(minutes) min" } cell.detailTextLabel?.text = detailText } return cell } public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return carbEntries[indexPath.row].createdByCurrentApp } public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete, case .display = state, let carbStore = carbStore { let entry = carbEntries.remove(at: indexPath.row) carbStore.deleteCarbEntry(entry) { (result) -> Void in DispatchQueue.main.async { switch result { case .failure(let error): self.present(UIAlertController(with: error), animated: true) case .success: tableView.deleteRows(at: [indexPath], with: .automatic) self.updateTimelyStats(nil) self.updateTotal() } } } } } // MARK: - UITableViewDelegate public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { let entry = carbEntries[indexPath.row] if !entry.createdByCurrentApp { return nil } return indexPath } // MARK: - Navigation @IBAction func unwindFromEditing(_ segue: UIStoryboardSegue) { if let editVC = segue.source as? CarbEntryEditViewController, let updatedEntry = editVC.updatedCarbEntry { if let originalEntry = editVC.originalCarbEntry { carbStore?.replaceCarbEntry(originalEntry, withEntry: updatedEntry) { (result) -> Void in DispatchQueue.main.async { switch result { case .failure(let error): self.present(UIAlertController(with: error), animated: true) case .success: self.reloadData() } } } } else { carbStore?.addCarbEntry(updatedEntry) { (result) -> Void in DispatchQueue.main.async { switch result { case .failure(let error): self.present(UIAlertController(with: error), animated: true) case .success: self.reloadData() } } } } } } public override func prepare(for segue: UIStoryboardSegue, sender: Any?) { var editVC = segue.destination as? CarbEntryEditViewController if editVC == nil, let navVC = segue.destination as? UINavigationController { editVC = navVC.viewControllers.first as? CarbEntryEditViewController } if let editVC = editVC { if let selectedCell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: selectedCell), indexPath.row < carbEntries.count { editVC.originalCarbEntry = carbEntries[indexPath.row] } editVC.defaultAbsorptionTimes = carbStore?.defaultAbsorptionTimes } } @IBAction func authorizeHealth(_ sender: Any) { if case .authorizationRequired = state, let carbStore = carbStore { carbStore.authorize { (result) in DispatchQueue.main.async { switch result { case .success: self.state = .display case .failure(let error): self.present(UIAlertController(with: error), animated: true) } } } } } }