| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- //
- // LegacyInsulinDeliveryTableViewController.swift
- // Naterade
- //
- // Created by Nathan Racklyeft on 1/30/16.
- // Copyright © 2016 Nathan Racklyeft. All rights reserved.
- //
- import UIKit
- import LoopKit
- private let ReuseIdentifier = "Right Detail"
- public final class LegacyInsulinDeliveryTableViewController: UITableViewController {
- @IBOutlet var needsConfigurationMessageView: ErrorBackgroundView!
- @IBOutlet weak var iobValueLabel: UILabel! {
- didSet {
- iobValueLabel.textColor = headerValueLabelColor
- }
- }
- @IBOutlet weak var iobDateLabel: UILabel!
- @IBOutlet weak var totalValueLabel: UILabel! {
- didSet {
- totalValueLabel.textColor = headerValueLabelColor
- }
- }
- @IBOutlet weak var totalDateLabel: UILabel!
- @IBOutlet weak var dataSourceSegmentedControl: UISegmentedControl! {
- didSet {
- let titleFont = UIFont.systemFont(ofSize: 15, weight: .semibold)
- dataSourceSegmentedControl.setTitleTextAttributes([NSAttributedString.Key.font: titleFont], for: .normal)
- dataSourceSegmentedControl.setTitle(NSLocalizedString("Event History", comment: "Segmented button title for insulin delivery log event history"), forSegmentAt: 0)
- dataSourceSegmentedControl.setTitle(NSLocalizedString("Reservoir", comment: "Segmented button title for insulin delivery log reservoir history"), forSegmentAt: 1)
- }
- }
- public var enableEntryDeletion: Bool = true
-
- public var doseStore: DoseStore? {
- didSet {
- if let doseStore = doseStore {
- doseStoreObserver = NotificationCenter.default.addObserver(forName: nil, object: doseStore, queue: OperationQueue.main, using: { [weak self] (note) -> Void in
- switch note.name {
- case DoseStore.valuesDidChange:
- if self?.isViewLoaded == true {
- self?.reloadData()
- }
- default:
- break
- }
- })
- } else {
- doseStoreObserver = nil
- }
- }
- }
-
- public var headerValueLabelColor: UIColor = .label
- private var updateTimer: Timer? {
- willSet {
- if let timer = updateTimer {
- timer.invalidate()
- }
- }
- }
- public override func viewDidLoad() {
- super.viewDidLoad()
- state = .display
- }
- 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
- }
- public override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- if tableView.isEditing {
- tableView.endEditing(true)
- }
- }
- deinit {
- if let observer = doseStoreObserver {
- NotificationCenter.default.removeObserver(observer)
- }
- }
- public override func setEditing(_ editing: Bool, animated: Bool) {
- super.setEditing(editing, animated: animated)
- if editing && enableEntryDeletion {
- let item = UIBarButtonItem(
- title: LocalizedString("Delete All", comment: "Button title to delete all objects"),
- style: .plain,
- target: self,
- action: #selector(confirmDeletion(_:))
- )
- navigationItem.setLeftBarButton(item, animated: true)
- } else {
- navigationItem.setLeftBarButton(nil, animated: true)
- }
- }
- // MARK: - Data
- private enum State {
- case unknown
- case unavailable(Error?)
- case display
- }
- private var state = State.unknown {
- didSet {
- if isViewLoaded {
- reloadData()
- }
- }
- }
- private enum DataSourceSegment: Int {
- case history = 0
- case reservoir
- }
- private enum Values {
- case reservoir([ReservoirValue])
- case history([PersistedPumpEvent])
- }
- // Not thread-safe
- private var values = Values.reservoir([]) {
- didSet {
- let count: Int
- switch values {
- case .reservoir(let values):
- count = values.count
- case .history(let values):
- count = values.count
- }
- if count > 0 && enableEntryDeletion {
- navigationItem.rightBarButtonItem = self.editButtonItem
- }
- }
- }
- private func reloadData() {
- switch state {
- case .unknown:
- break
- case .unavailable(let error):
- self.tableView.tableHeaderView?.isHidden = true
- self.tableView.tableFooterView = UIView()
- tableView.backgroundView = needsConfigurationMessageView
- if let error = error {
- needsConfigurationMessageView.errorDescriptionLabel.text = String(describing: error)
- } else {
- needsConfigurationMessageView.errorDescriptionLabel.text = nil
- }
- case .display:
- self.tableView.backgroundView = nil
- self.tableView.tableHeaderView?.isHidden = false
- self.tableView.tableFooterView = nil
- switch DataSourceSegment(rawValue: dataSourceSegmentedControl.selectedSegmentIndex)! {
- case .reservoir:
- doseStore?.getReservoirValues(since: Date.distantPast) { (result) in
- DispatchQueue.main.async { () -> Void in
- switch result {
- case .failure(let error):
- self.state = .unavailable(error)
- case .success(let reservoirValues):
- self.values = .reservoir(reservoirValues)
- self.tableView.reloadData()
- }
- }
- self.updateTimelyStats(nil)
- self.updateTotal()
- }
- case .history:
- doseStore?.getPumpEventValues(since: Date.distantPast) { (result) in
- DispatchQueue.main.async { () -> Void in
- switch result {
- case .failure(let error):
- self.state = .unavailable(error)
- case .success(let pumpEventValues):
- self.values = .history(pumpEventValues)
- self.tableView.reloadData()
- }
- }
- self.updateTimelyStats(nil)
- self.updateTotal()
- }
- }
- }
- }
- @objc func updateTimelyStats(_: Timer?) {
- updateIOB()
- }
- private lazy var iobNumberFormatter: NumberFormatter = {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.maximumFractionDigits = 2
- return formatter
- }()
- private lazy var timeFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .none
- formatter.timeStyle = .short
- return formatter
- }()
- private func updateIOB() {
- if case .display = state {
- doseStore?.insulinOnBoard(at: Date()) { (result) -> Void in
- DispatchQueue.main.async {
- switch result {
- case .failure:
- self.iobValueLabel.text = "…"
- self.iobDateLabel.text = nil
- case .success(let iob):
- self.iobValueLabel.text = self.iobNumberFormatter.string(from: iob.value)
- self.iobDateLabel.text = String(format: LocalizedString("com.loudnate.InsulinKit.IOBDateLabel", value: "at %1$@", comment: "The format string describing the date of an IOB value. The first format argument is the localized date."), self.timeFormatter.string(from: iob.startDate))
- }
- }
- }
- }
- }
- private func updateTotal() {
- if case .display = state {
- let midnight = Calendar.current.startOfDay(for: Date())
- doseStore?.getTotalUnitsDelivered(since: midnight) { (result) in
- DispatchQueue.main.async {
- switch result {
- case .failure:
- self.totalValueLabel.text = "…"
- self.totalDateLabel.text = nil
- case .success(let result):
- self.totalValueLabel.text = NumberFormatter.localizedString(from: NSNumber(value: result.value), number: .none)
- self.totalDateLabel.text = String(format: LocalizedString("com.loudnate.InsulinKit.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: result.startDate, dateStyle: .none, timeStyle: .short))
- }
- }
- }
- }
- }
- private var doseStoreObserver: Any? {
- willSet {
- if let observer = doseStoreObserver {
- NotificationCenter.default.removeObserver(observer)
- }
- }
- }
- @IBAction func selectedSegmentChanged(_ sender: Any) {
- reloadData()
- }
- @IBAction func confirmDeletion(_ sender: Any) {
- guard !deletionPending else {
- return
- }
- let confirmMessage: String
- switch DataSourceSegment(rawValue: dataSourceSegmentedControl.selectedSegmentIndex)! {
- case .reservoir:
- confirmMessage = LocalizedString("Are you sure you want to delete all reservoir values?", comment: "Action sheet confirmation message for reservoir deletion")
- case .history:
- confirmMessage = LocalizedString("Are you sure you want to delete all history entries?", comment: "Action sheet confirmation message for pump history deletion")
- }
- let sheet = UIAlertController(deleteAllConfirmationMessage: confirmMessage) {
- self.deleteAllObjects()
- }
- present(sheet, animated: true)
- }
- private var deletionPending = false
- private func deleteAllObjects() {
- guard !deletionPending else {
- return
- }
- deletionPending = true
- let completion = { (_: DoseStore.DoseStoreError?) -> Void in
- DispatchQueue.main.async {
- self.deletionPending = false
- self.setEditing(false, animated: true)
- }
- }
- switch DataSourceSegment(rawValue: dataSourceSegmentedControl.selectedSegmentIndex)! {
- case .reservoir:
- doseStore?.deleteAllReservoirValues(completion)
- case .history:
- doseStore?.deleteAllPumpEvents(completion)
- }
- }
- // MARK: - Table view data source
- public override func numberOfSections(in tableView: UITableView) -> Int {
- switch state {
- case .unknown, .unavailable:
- return 0
- case .display:
- return 1
- }
- }
- public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- switch values {
- case .reservoir(let values):
- return values.count
- case .history(let values):
- return values.count
- }
- }
- public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier, for: indexPath)
- if case .display = state {
- switch self.values {
- case .reservoir(let values):
- let entry = values[indexPath.row]
- let volume = NumberFormatter.localizedString(from: NSNumber(value: entry.unitVolume), number: .decimal)
- let time = timeFormatter.string(from: entry.startDate)
- cell.textLabel?.text = String(format: NSLocalizedString("%1$@ U", comment: "Reservoir entry (1: volume value)"), volume)
- cell.textLabel?.textColor = .label
- cell.detailTextLabel?.text = time
- cell.accessoryType = .none
- cell.selectionStyle = .none
- case .history(let values):
- let entry = values[indexPath.row]
- let time = timeFormatter.string(from: entry.date)
- if let attributedText = entry.dose?.localizedAttributedDescription {
- cell.textLabel?.attributedText = attributedText
- } else {
- cell.textLabel?.text = entry.title
- }
-
- cell.detailTextLabel?.text = time
- cell.accessoryType = entry.isUploaded ? .checkmark : .none
- cell.selectionStyle = .default
- }
- }
- return cell
- }
- public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
- return enableEntryDeletion
- }
- public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- if editingStyle == .delete, case .display = state {
- switch values {
- case .reservoir(let reservoirValues):
- var reservoirValues = reservoirValues
- let value = reservoirValues.remove(at: indexPath.row)
- self.values = .reservoir(reservoirValues)
- tableView.deleteRows(at: [indexPath], with: .automatic)
- doseStore?.deleteReservoirValue(value) { (_, error) -> Void in
- if let error = error {
- DispatchQueue.main.async {
- self.present(UIAlertController(with: error), animated: true)
- self.reloadData()
- }
- }
- }
- case .history(let historyValues):
- var historyValues = historyValues
- let value = historyValues.remove(at: indexPath.row)
- self.values = .history(historyValues)
- tableView.deleteRows(at: [indexPath], with: .automatic)
- doseStore?.deletePumpEvent(value) { (error) -> Void in
- if let error = error {
- DispatchQueue.main.async {
- self.present(UIAlertController(with: error), animated: true)
- self.reloadData()
- }
- }
- }
- }
- }
- }
- public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- if case .display = state, case .history(let history) = values {
- let entry = history[indexPath.row]
- let vc = CommandResponseViewController(command: { (completionHandler) -> String in
- var description = [String]()
- description.append(self.timeFormatter.string(from: entry.date))
- if let title = entry.title {
- description.append(title)
- }
- if let dose = entry.dose {
- description.append(String(describing: dose))
- }
- if let raw = entry.raw {
- description.append(raw.hexadecimalString)
- }
- return description.joined(separator: "\n\n")
- })
- vc.title = LocalizedString("Pump Event", comment: "The title of the screen displaying a pump event")
- show(vc, sender: indexPath)
- }
- }
- }
- fileprivate extension UIAlertController {
- convenience init(deleteAllConfirmationMessage: String, confirmationHandler handler: @escaping () -> Void) {
- self.init(
- title: nil,
- message: deleteAllConfirmationMessage,
- preferredStyle: .actionSheet
- )
-
- addAction(UIAlertAction(
- title: LocalizedString("Delete All", comment: "Button title to delete all objects"),
- style: .destructive,
- handler: { (_) in handler() }
- ))
- addAction(UIAlertAction(
- title: LocalizedString("Cancel", comment: "The title of the cancel action in an action sheet"),
- style: .cancel
- ))
- }
- }
- extension DoseEntry {
- fileprivate var numberFormatter: NumberFormatter {
- let numberFormatter = NumberFormatter()
- numberFormatter.maximumFractionDigits = DoseEntry.unitsPerHour.maxFractionDigits
- return numberFormatter
- }
- fileprivate var localizedAttributedDescription: NSAttributedString? {
- let font = UIFont.preferredFont(forTextStyle: .body)
- switch type {
- case .bolus:
- let description: String
- if let deliveredUnits = deliveredUnits,
- deliveredUnits != programmedUnits
- {
- description = String(format: NSLocalizedString("Interrupted %1$@: <b>%2$@</b> of %3$@ %4$@", comment: "Description of an interrupted bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: programmed value (? if no value), 4: unit)"), type.localizedDescription, numberFormatter.string(from: deliveredUnits) ?? "?", numberFormatter.string(from: programmedUnits) ?? "?", DoseEntry.units.shortLocalizedUnitString())
- } else {
- description = String(format: NSLocalizedString("%1$@: <b>%2$@</b> %3$@", comment: "Description of a bolus dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit)"), type.localizedDescription, numberFormatter.string(from: programmedUnits) ?? "?", DoseEntry.units.shortLocalizedUnitString())
- }
- return createAttributedDescription(from: description, with: font)
- case .basal, .tempBasal:
- let description = String(format: NSLocalizedString("%1$@: <b>%2$@</b> %3$@", comment: "Description of a basal temp basal dose entry (1: title for dose type, 2: value (? if no value) in bold, 3: unit)"), type.localizedDescription, numberFormatter.string(from: unitsPerHour) ?? "?", DoseEntry.unitsPerHour.shortLocalizedUnitString())
- return createAttributedDescription(from: description, with: font)
- case .suspend, .resume:
- let attributes: [NSAttributedString.Key: Any] = [
- .font: font,
- .foregroundColor: UIColor.secondaryLabel
- ]
- return NSAttributedString(string: type.localizedDescription, attributes: attributes)
- }
- }
- fileprivate func createAttributedDescription(from description: String, with font: UIFont) -> NSAttributedString? {
- let descriptionWithFont = String(format:"<style>body{font-family: '-apple-system', '\(font.fontName)'; font-size: \(font.pointSize);}</style>%@", description)
- guard let attributedDescription = try? NSMutableAttributedString(data: Data(descriptionWithFont.utf8), options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: NSNumber(value: String.Encoding.utf8.rawValue)], documentAttributes: nil)
- else {
- return nil
- }
- attributedDescription.enumerateAttribute(.font, in: NSRange(location: 0, length: attributedDescription.length)) { value, range, stop in
- // bold font items have a dominate colour
- if let font = value as? UIFont,
- font.fontDescriptor.symbolicTraits.contains(.traitBold)
- {
- attributedDescription.addAttributes([.foregroundColor: UIColor.label], range: range)
- } else {
- attributedDescription.addAttributes([.foregroundColor: UIColor.secondaryLabel], range: range)
- }
- }
- return attributedDescription
- }
- }
|