| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- //
- // GlucoseRangeScheduleTableViewController.swift
- // Naterade
- //
- // Created by Nathan Racklyeft on 2/13/16.
- // Copyright © 2016 Nathan Racklyeft. All rights reserved.
- //
- import UIKit
- import HealthKit
- import LoopKit
- public enum SaveGlucoseRangeScheduleResult {
- case success
- case failure(Error)
- }
- public protocol GlucoseRangeScheduleStorageDelegate {
- func saveSchedule(for viewController: GlucoseRangeScheduleTableViewController, completion: @escaping (_ result: SaveGlucoseRangeScheduleResult) -> Void)
- }
- private struct EditableRange {
- public let minValue: Double?
- public let maxValue: Double?
- init(minValue: Double?, maxValue: Double?) {
- self.minValue = minValue
- self.maxValue = maxValue
- }
- }
- public class GlucoseRangeScheduleTableViewController: UITableViewController {
- public init(allowedValues: [Double], unit: HKUnit, minimumTimeInterval: TimeInterval = TimeInterval(30 * 60)) {
- self.allowedValues = allowedValues
- self.unit = unit
- self.minimumTimeInterval = minimumTimeInterval
- super.init(style: .grouped)
- }
- public required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- public override func viewDidLoad() {
- super.viewDidLoad()
- tableView.register(GlucoseRangeTableViewCell.nib(), forCellReuseIdentifier: GlucoseRangeTableViewCell.className)
- tableView.register(GlucoseRangeOverrideTableViewCell.nib(), forCellReuseIdentifier: GlucoseRangeOverrideTableViewCell.className)
- tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
- navigationItem.rightBarButtonItems = [insertButtonItem, editButtonItem]
- updateEditButton()
- }
- @objc private func cancel(_ sender: Any?) {
- self.navigationController?.popViewController(animated: true)
- }
- public private(set) lazy var insertButtonItem: UIBarButtonItem = {
- return UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addScheduleItem(_:)))
- }()
- private func updateInsertButton() {
- guard let lastItem = editableItems.last else {
- return
- }
- insertButtonItem.isEnabled = !isEditing && lastItem.startTime < lastValidStartTime
- }
- open override func setEditing(_ editing: Bool, animated: Bool) {
- tableView.beginUpdates()
- hideGlucoseRangeCells()
- tableView.endUpdates()
- super.setEditing(editing, animated: animated)
- updateInsertButton()
- updateSaveButton()
- }
- private func updateSaveButton() {
- if let section = sections.firstIndex(of: .save), let cell = tableView.cellForRow(at: IndexPath(row: 0, section: section)) as? TextButtonTableViewCell {
- cell.isEnabled = !isEditing && isScheduleModified && isScheduleValid
- }
- }
- private var isScheduleValid: Bool {
- return !editableItems.isEmpty &&
- editableItems.allSatisfy { isValid($0.value) }
- }
- private func updateEditButton() {
- editButtonItem.isEnabled = editableItems.endIndex > 1
- }
- public func setSchedule(_ schedule: GlucoseRangeSchedule, withOverrideRanges overrides: [TemporaryScheduleOverride.Context: DoubleRange]) {
- unit = schedule.unit
- editableItems = schedule.items.map({ (item) -> RepeatingScheduleValue<EditableRange> in
- let range = EditableRange(minValue: item.value.minValue, maxValue: item.value.maxValue)
- return RepeatingScheduleValue<EditableRange>(startTime: item.startTime, value: range)
- })
- editableOverrideRanges.removeAll()
- for (context, range) in overrides {
- editableOverrideRanges[context] = EditableRange(minValue: range.minValue, maxValue: range.maxValue)
- }
- isScheduleModified = false
- }
- private func isValid(_ range: EditableRange) -> Bool {
- guard let max = range.maxValue, let min = range.minValue, min <= max else {
- return false
- }
- return allowedValues.contains(max) && allowedValues.contains(min)
- }
- @IBAction func addScheduleItem(_ sender: Any?) {
- guard let allowedTimeRange = allowedTimeRange(for: editableItems.count) else {
- return
- }
- editableItems.append(
- RepeatingScheduleValue(
- startTime: allowedTimeRange.lowerBound,
- value: editableItems.last?.value ?? EditableRange(minValue: nil, maxValue: nil)
- )
- )
- tableView.beginUpdates()
- tableView.insertRows(at: [IndexPath(row: editableItems.count - 1, section: Section.schedule.rawValue)], with: .automatic)
- if editableItems.count == 1 {
- tableView.insertSections(IndexSet(integer: Section.override.rawValue), with: .automatic)
- }
- tableView.endUpdates()
- }
- private func updateTimeLimits(for index: Int) {
- let indexPath = IndexPath(row: index, section: Section.schedule.rawValue)
- if let allowedTimeRange = allowedTimeRange(for: index), let cell = tableView.cellForRow(at: indexPath) as? GlucoseRangeTableViewCell {
- cell.allowedTimeRange = allowedTimeRange
- }
- }
- private func allowedTimeRange(for index: Int) -> ClosedRange<TimeInterval>? {
- let minTime: TimeInterval
- let maxTime: TimeInterval
- if index == 0 {
- maxTime = TimeInterval(0)
- } else if index+1 < editableItems.endIndex {
- maxTime = editableItems[index+1].startTime - minimumTimeInterval
- } else {
- maxTime = lastValidStartTime
- }
- if index > 0 {
- minTime = editableItems[index-1].startTime + minimumTimeInterval
- if minTime > lastValidStartTime {
- return nil
- }
- } else {
- minTime = TimeInterval(0)
- }
- return minTime...maxTime
- }
- func insertableIndices(removing row: Int) -> [Bool] {
- let insertableIndices = editableItems.enumerated().map { (enumeration) -> Bool in
- let (index, item) = enumeration
- if row == index {
- return true
- } else if index == 0 {
- return false
- } else if index == editableItems.endIndex - 1 {
- return item.startTime < TimeInterval(hours: 24) - minimumTimeInterval
- } else if index > row {
- return editableItems[index + 1].startTime - item.startTime > minimumTimeInterval
- } else {
- return item.startTime - editableItems[index - 1].startTime > minimumTimeInterval
- }
- }
- return insertableIndices
- }
- // MARK: - State
- public var delegate: GlucoseRangeScheduleStorageDelegate?
- let allowedValues: [Double]
- let minimumTimeInterval: TimeInterval
- var lastValidStartTime: TimeInterval {
- return TimeInterval.hours(24) - minimumTimeInterval
- }
- public var timeZone = TimeZone.currentFixed
- private var unit: HKUnit = HKUnit.milligramsPerDeciliter
- private var isScheduleModified = false {
- didSet {
- if isScheduleModified {
- self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(_:)))
- } else {
- self.navigationItem.leftBarButtonItem = nil
- }
- updateSaveButton()
- }
- }
- private var editableItems: [RepeatingScheduleValue<EditableRange>] = [] {
- didSet {
- isScheduleModified = true
- updateInsertButton()
- updateEditButton()
- }
- }
- public var schedule: GlucoseRangeSchedule? {
- get {
- let dailyItems = editableItems.compactMap { (item) -> RepeatingScheduleValue<DoubleRange>? in
- guard isValid(item.value) else {
- return nil
- }
- guard let min = item.value.minValue, let max = item.value.maxValue else {
- return nil
- }
- let range = DoubleRange(minValue: min, maxValue: max)
- return RepeatingScheduleValue(startTime: item.startTime, value: range)
- }
- return GlucoseRangeSchedule(unit: unit, dailyItems: dailyItems)
- }
- }
- public var overrideContexts: [TemporaryScheduleOverride.Context] = [.preMeal, .legacyWorkout]
- private var editableOverrideRanges: [TemporaryScheduleOverride.Context: EditableRange] = [:] {
- didSet {
- isScheduleModified = true
- }
- }
- public var overrideRanges: [TemporaryScheduleOverride.Context: DoubleRange] {
- get {
- var setRanges: [TemporaryScheduleOverride.Context: DoubleRange] = [:]
- for (context, range) in editableOverrideRanges {
- if let minValue = range.minValue, let maxValue = range.maxValue, isValid(range) {
- setRanges[context] = DoubleRange(minValue: minValue, maxValue: maxValue)
- }
- }
- return setRanges
- }
- }
- // MARK: - UITableViewDataSource
- private enum Section: Int, CaseIterable {
- case schedule = 0
- case override
- case save
- }
- private var showOverrides: Bool {
- return !editableItems.isEmpty
- }
- private var sections: [Section] {
- if !showOverrides {
- return [.schedule, .save]
- } else {
- return Section.allCases
- }
- }
- public override func numberOfSections(in tableView: UITableView) -> Int {
- return sections.count
- }
- public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- switch sections[section] {
- case .schedule:
- return editableItems.count
- case .override:
- return overrideContexts.count
- case .save:
- return 1
- }
- }
- public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- switch sections[indexPath.section] {
- case .schedule:
- let cell = tableView.dequeueReusableCell(withIdentifier: GlucoseRangeTableViewCell.className, for: indexPath) as! GlucoseRangeTableViewCell
- let item = editableItems[indexPath.row]
- cell.timeZone = timeZone
- cell.startTime = item.startTime
- cell.allowedValues = allowedValues
- cell.valueNumberFormatter.minimumFractionDigits = unit.preferredFractionDigits
- cell.valueNumberFormatter.maximumFractionDigits = unit.preferredFractionDigits
- cell.minValue = item.value.minValue
- cell.maxValue = item.value.maxValue
- cell.unitString = unit.shortLocalizedUnitString()
- cell.delegate = self
- if let allowedTimeRange = allowedTimeRange(for: indexPath.row) {
- cell.allowedTimeRange = allowedTimeRange
- }
- return cell
- case .override:
- let cell = tableView.dequeueReusableCell(withIdentifier: GlucoseRangeOverrideTableViewCell.className, for: indexPath) as! GlucoseRangeOverrideTableViewCell
- cell.valueNumberFormatter.minimumFractionDigits = unit.preferredFractionDigits
- cell.valueNumberFormatter.maximumFractionDigits = unit.preferredFractionDigits
- cell.allowedValues = allowedValues
- let context = overrideContexts[indexPath.row]
- if let range = overrideRanges[context], !range.isZero {
- cell.minValue = range.minValue
- cell.maxValue = range.maxValue
- }
- let bundle = Bundle(for: type(of: self))
- let titleText: String
- let image: UIImage?
- switch context {
- case .legacyWorkout:
- titleText = LocalizedString("Workout", comment: "Title for the workout override range")
- image = UIImage(named: "workout", in: bundle, compatibleWith: traitCollection)
- case .preMeal:
- titleText = LocalizedString("Pre-Meal", comment: "Title for the pre-meal override range")
- image = UIImage(named: "Pre-Meal", in: bundle, compatibleWith: traitCollection)
- default:
- preconditionFailure("Unexpected override context \(context)")
- }
- cell.dateLabel.text = titleText
- cell.iconImageView.image = image
- cell.unitString = unit.shortLocalizedUnitString()
- cell.delegate = self
- return cell
- case .save:
- let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
- cell.textLabel?.text = LocalizedString("Save", comment: "Button text for saving glucose correction range schedule")
- cell.isEnabled = isScheduleModified
- return cell
- }
- }
- public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- if editingStyle == .delete, let overrideSectionIndex = sections.firstIndex(of: .override) {
- editableItems.remove(at: indexPath.row)
- tableView.performBatchUpdates({
- tableView.deleteRows(at: [indexPath], with: .automatic)
- if editableItems.isEmpty {
- tableView.deleteSections(IndexSet(integer: overrideSectionIndex), with: .automatic)
- }
- }, completion: nil)
- if editableItems.count == 1 {
- setEditing(false, animated: true)
- }
- updateSaveButton()
- }
- }
- public override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
- if sourceIndexPath != destinationIndexPath {
- switch sections[destinationIndexPath.section] {
- case .schedule:
- let item = editableItems.remove(at: sourceIndexPath.row)
- editableItems.insert(item, at: destinationIndexPath.row)
- guard destinationIndexPath.row > 0 else {
- return
- }
- let startTime = editableItems[destinationIndexPath.row - 1].startTime + minimumTimeInterval
- editableItems[destinationIndexPath.row] = RepeatingScheduleValue(startTime: startTime, value: editableItems[destinationIndexPath.row].value)
- // Since the valid date ranges of neighboring cells are affected, the lazy solution is to just reload the entire table view
- DispatchQueue.main.async {
- tableView.reloadData()
- }
- case .override, .save:
- break
- }
- }
- }
- public override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
- switch sections[indexPath.section] {
- case .schedule:
- return indexPath.row > 0
- default:
- return false
- }
- }
- public override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
- switch sections[indexPath.section] {
- case .schedule:
- return true
- default:
- return false
- }
- }
- public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
- switch sections[section] {
- case .override:
- return LocalizedString("Overrides", comment: "The section title of glucose overrides")
- default:
- return nil
- }
- }
- public override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
- switch sections[section] {
- case .schedule:
- return LocalizedString("Correction range is the blood glucose range that you would like Loop to correct to.", comment: "The section footer of correction range schedule")
- default:
- return nil
- }
- }
- // MARK: - UITableViewDelegate
- public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
- return true
- }
- public override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
- tableView.beginUpdates()
- switch sections[indexPath.section] {
- case .schedule:
- updateTimeLimits(for: indexPath.row)
- hideGlucoseRangeCells(excluding: indexPath)
- case .override:
- hideGlucoseRangeCells(excluding: indexPath)
- default:
- break
- }
- tableView.endEditing(false)
- return indexPath
- }
- public override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
- tableView.beginUpdates()
- tableView.endUpdates()
- }
- public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- switch sections[indexPath.section] {
- case .save:
- delegate?.saveSchedule(for: self, completion: { (result) in
- switch result {
- case .success:
- self.isScheduleModified = false
- self.updateInsertButton()
- case .failure(let error):
- self.present(UIAlertController(with: error), animated: true)
- }
- })
- default:
- break
- }
- tableView.endEditing(false)
- tableView.endUpdates()
- tableView.deselectRow(at: indexPath, animated: true)
- }
- public override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
- guard sourceIndexPath.section == proposedDestinationIndexPath.section else {
- return sourceIndexPath
- }
- guard sourceIndexPath != proposedDestinationIndexPath else {
- return proposedDestinationIndexPath
- }
- let indices = insertableIndices(removing: sourceIndexPath.row)
- let closestDestinationRow = indices.insertableIndex(closestTo: proposedDestinationIndexPath.row, from: sourceIndexPath.row)
- return IndexPath(row: closestDestinationRow, section: proposedDestinationIndexPath.section)
- }
- }
- extension GlucoseRangeScheduleTableViewController : GlucoseRangeTableViewCellDelegate {
- func glucoseRangeTableViewCellDidUpdate(_ cell: GlucoseRangeTableViewCell) {
- if let indexPath = tableView.indexPath(for: cell) {
- switch sections[indexPath.section] {
- case .schedule:
- editableItems[indexPath.row] = RepeatingScheduleValue(
- startTime: cell.startTime,
- value: EditableRange(minValue: cell.minValue, maxValue: cell.maxValue)
- )
- case .override:
- let context = overrideContexts[indexPath.row]
- editableOverrideRanges[context] = EditableRange(minValue: cell.minValue, maxValue: cell.maxValue)
- default:
- break
- }
- }
- }
- }
|